Browser privacy proxies and IP protection settings can mask client IP addresses on third-party ad requests to Google Ad Manager. Masked IP addresses can reduce geographic targeting accuracy and ad relevance. Because your web server or content delivery network (CDN) interacts with the user in a first-party context, your server receives the user's unmasked IP address.
The publisher-provided IP feature lets you capture the user's unmasked IPv4 or IPv6 address on your server, encrypt the address using Hybrid Public Key Encryption (HPKE), and pass the encrypted payload to Google Publisher Tag (GPT) on the client.
This page covers integrating publisher-provided IP with GPT.
Implement the server-side workflow
To avoid exposing the user's plaintext IP address to third-party scripts in the browser, perform the following steps on your web server or CDN edge worker:
- Perform all IP capture.
- Fetch the public key.
- Perform HPKE encryption.
Capture the client IP address
Extract the client's unmasked IPv4 or IPv6 address from the incoming page request on your web server or CDN:
- Direct connections: Read the remote socket address of the incoming HTTP connection.
- Behind a CDN, load balancer, or reverse proxy: Read the client IP header
from your trusted edge proxy, such as the
X-Forwarded-Forheader or a CDN-specific client IP header. Only trust forwarding headers that your proxy infrastructure adds or overrides. When theX-Forwarded-Forheader contains multiple comma-separated addresses, select the client IP that the trusted edge proxy added.
Ad Manager supports the following addresses:
- IPv4, for example,
203.0.113.42. - IPv6, for example,
2001:db8::8a2e:370:7334.
Pass only the bare IP address string, without port numbers, brackets, CIDR prefixes, or surrounding whitespace.
Fetch and cache the public key
Google publishes the active HPKE encryption key as an RFC 8037 JSON Web Key Set (JWKS) at the following endpoint:
https://www.gstatic.com/real-ip/ip_enc_key.json
A GET request to this endpoint returns a JSON object, whose keys array
contains a single active key object. Use the key in that object's x
property to encrypt the client IP address.
The following example fetches the public key and extracts the x property on
success and throws an exception on failure:
Java
private static final String KEY_ENDPOINT_URL = "https://www.gstatic.com/real-ip/ip_enc_key.json";
/**
* Fetches the active X25519 public key from Google's key endpoint.
*
* <p>In production, cache the returned public key in memory and refresh it at least once every 24
* hours rather than fetching it on every incoming request.
*
* @return The unpadded Base64URL-encoded 32-byte X25519 public key ('x').
*/
public static String fetchPublicKey() throws IOException, InterruptedException {
return fetchPublicKey(KEY_ENDPOINT_URL);
}
public static String fetchPublicKey(String endpointUrl) throws IOException, InterruptedException {
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(endpointUrl))
.timeout(Duration.ofSeconds(5))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new IOException("Failed to fetch public key, HTTP status: " + response.statusCode());
}
JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject();
JsonArray keys = json.getAsJsonArray("keys");
if (keys == null || keys.isEmpty()) {
throw new IOException("No keys found in response from " + endpointUrl);
}
JsonObject keyObj = keys.get(0).getAsJsonObject();
if (!keyObj.has("x")) {
throw new IOException("Key payload missing 'x' property: " + keyObj);
}
return keyObj.get("x").getAsString();
}
Python
KEY_ENDPOINT_URL = "https://www.gstatic.com/real-ip/ip_enc_key.json"
def fetch_public_key(url: str = KEY_ENDPOINT_URL) -> str:
"""Fetches the active X25519 public key from Google's key endpoint.
In production, cache the returned public key in memory and refresh it at
least once every 24 hours rather than fetching it on every incoming request.
Args:
url: URL of the public key endpoint.
Returns:
The unpadded Base64URL-encoded 32-byte X25519 public key ('x').
"""
with urllib.request.urlopen(url, timeout=5) as response:
data = json.loads(response.read().decode("utf-8"))
keys = data.get("keys", [])
if not keys:
raise ValueError(f"No keys found in response from {url}")
key_payload = keys[0].get("x")
if not key_payload:
raise ValueError(f"Key payload missing 'x' property: {keys[0]}")
return key_payload
JavaScript
const KEY_ENDPOINT_URL = 'https://www.gstatic.com/real-ip/ip_enc_key.json';
/**
* Fetches the active X25519 public key from Google's key endpoint.
*
* In production, cache the returned public key in memory and refresh it at
* least once every 24 hours rather than fetching it on every incoming request.
*
* @param {string=} url URL of the public key endpoint.
* @return {!Promise<string>} The unpadded Base64URL-encoded 32-byte X25519
* public key ('x').
*/
async function fetchPublicKey(url = KEY_ENDPOINT_URL) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(
`Failed to fetch public key, HTTP status: ${response.status}`,
);
}
const data = await response.json();
if (!data.keys || data.keys.length === 0) {
throw new Error(`No keys found in response from ${url}`);
}
const keyPayload = data.keys[0].x;
if (!keyPayload) {
throw new Error(
`Key payload missing 'x' property: ${JSON.stringify(data.keys[0])}`,
);
}
return keyPayload;
}
TypeScript
const KEY_ENDPOINT_URL = 'https://www.gstatic.com/real-ip/ip_enc_key.json';
interface JwkKey {
kty?: string;
crv?: string;
x?: string;
kid?: string;
}
interface KeyEndpointResponse {
keys?: JwkKey[];
}
/**
* Fetches the active X25519 public key from Google's key endpoint.
*
* In production, cache the returned public key in memory and refresh it at
* least once every 24 hours rather than fetching it on every incoming request.
*
* @param url URL of the public key endpoint.
* @return The unpadded Base64URL-encoded 32-byte X25519 public key ('x').
*/
export async function fetchPublicKey(
url: string = KEY_ENDPOINT_URL,
): Promise<string> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(
`Failed to fetch public key, HTTP status: ${response.status}`,
);
}
const data = (await response.json()) as KeyEndpointResponse;
if (!data.keys || data.keys.length === 0) {
throw new Error(`No keys found in response from ${url}`);
}
const keyPayload = data.keys[0].x;
if (!keyPayload) {
throw new Error(
`Key payload missing 'x' property: ${JSON.stringify(data.keys[0])}`,
);
}
return keyPayload;
}
If successful, the response includes the keys array containing a key object
with the x property:
{
"keys": [
{
"kty": "OKP",
"crv": "X25519",
"x": "7bws5TIHOUrcJNofEubW3DPW8zR9EtjFNLm_Grv98TA",
"kid": "EZMLFw"
}
]
}
The following table describes the properties of the key object in the keys
array:
| Property | Description |
|---|---|
crv |
Elliptic curve identifier. Always X25519. |
kid |
Unpadded base64url-encoded key identifier, which changes when Google rotates the key. |
kty |
Key type. Always OKP (Octet Key Pair). |
x |
Unpadded base64url-encoded 32-byte raw X25519 public key used for encryption. |
Cache the fetched public key in memory on your server and refresh the key at
least once every 24 hours, or sooner according to the HTTP Cache-Control
response header. Don't fetch the public key on every incoming page request.
Google retains rotated private keys. Payloads encrypted with a public key cached
in the preceding 24 hours continue to decrypt without interruption during key
rotation.
Encrypt the IP address
Encrypt the UTF-8 byte representation of the client's IP address string by using
HPKE (RFC 9180) and the decoded 32-byte X25519 public key, x,
retrieved in Fetch and cache the public key.
The following example encrypts an IP address string:
Java
This example uses Tink (com.google.crypto.tink:tink 1.16.0 or
higher):
static {
try {
HybridConfig.register();
} catch (GeneralSecurityException e) {
throw new ExceptionInInitializerError(e);
}
}
/**
* Encrypts an IP address using HPKE (DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-256-GCM).
*
* @param pubXB64Url Base64URL-encoded 32-byte X25519 public key ('x').
* @param ipStr Plaintext IPv4 or IPv6 address string (for example, "203.0.113.42").
* @return Unpadded Base64URL-encoded payload (enc || ciphertext).
*/
@AccessesPartialKey
public static String encryptIp(String pubXB64Url, String ipStr) throws GeneralSecurityException {
// 1. Decode the 32-byte X25519 public key 'x' from Base64URL.
byte[] pubBytes = Base64.getUrlDecoder().decode(pubXB64Url);
if (pubBytes.length != 32) {
throw new IllegalArgumentException(
"Public key must be exactly 32 bytes, got " + pubBytes.length);
}
// 2. Configure HPKE parameters (DHKEM_X25519_HKDF_SHA256, HKDF_SHA256,
// AES_256_GCM, NO_PREFIX).
HpkeParameters parameters =
HpkeParameters.builder()
.setKemId(HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256)
.setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
.setAeadId(HpkeParameters.AeadId.AES_256_GCM)
.setVariant(HpkeParameters.Variant.NO_PREFIX)
.build();
HpkePublicKey publicKey =
HpkePublicKey.create(parameters, Bytes.copyFrom(pubBytes), /* idRequirement= */ null);
// 3. Encrypt the UTF-8 IP address bytes. With Variant.NO_PREFIX, Tink
// outputs raw (enc || ciphertext_with_tag).
KeysetHandle handle =
KeysetHandle.newBuilder()
.addEntry(KeysetHandle.importKey(publicKey).withRandomId().makePrimary())
.build();
HybridEncrypt encryptor = handle.getPrimitive(RegistryConfiguration.get(), HybridEncrypt.class);
byte[] payload =
encryptor.encrypt(ipStr.getBytes(StandardCharsets.UTF_8), /* contextInfo= */ new byte[0]);
// 4. Return unpadded Base64URL-encoded payload.
return Base64.getUrlEncoder().withoutPadding().encodeToString(payload);
}
Python
This example uses pyhpke:
def encrypt_ip(pub_x_b64url: str, ip_str: str) -> str:
"""Encrypts an IP address using HPKE (RFC 9180).
Args:
pub_x_b64url: Base64URL-encoded 32-byte X25519 public key ('x').
ip_str: Plaintext IPv4 or IPv6 address string (for example, "203.0.113.42").
Returns:
Unpadded Base64URL-encoded payload (enc || ciphertext).
"""
# 1. Decode the 32-byte X25519 public key 'x' from Base64URL.
pub_bytes = base64.urlsafe_b64decode(
pub_x_b64url + "=" * (-len(pub_x_b64url) % 4)
)
if len(pub_bytes) != 32:
raise ValueError(
f"Public key must be exactly 32 bytes, got {len(pub_bytes)}"
)
# 2. Configure HPKE cipher suite (DHKEM_X25519_HKDF_SHA256, HKDF_SHA256,
# AES256_GCM).
suite = CipherSuite.new(
KEMId.DHKEM_X25519_HKDF_SHA256, KDFId.HKDF_SHA256, AEADId.AES256_GCM
)
public_key = suite.kem.deserialize_public_key(pub_bytes)
# 3. Create a fresh sender context per encryption (you may cache 'suite' and
# 'public_key', but never reuse 'sender' across multiple seal() calls) and
# package (enc || ciphertext).
enc, sender = suite.create_sender_context(public_key)
ciphertext = sender.seal(ip_str.encode("utf-8"))
# 4. Return unpadded Base64URL-encoded payload.
return base64.urlsafe_b64encode(enc + ciphertext).decode("utf-8").rstrip("=")
JavaScript
This example uses @hpke/core:
/**
* Encrypts an IP address using HPKE (RFC 9180).
*
* @param {string} pubXB64Url Base64URL-encoded 32-byte X25519 public key ('x').
* @param {string} ipStr Plaintext IPv4 or IPv6 address string (for example,
* "203.0.113.42").
* @return {!Promise<string>} Unpadded Base64URL-encoded payload
* (enc || ciphertext).
*/
async function encryptIp(pubXB64Url, ipStr) {
// 1. Decode the 32-byte X25519 public key 'x' from Base64URL.
const pubBytes = Buffer.from(pubXB64Url, 'base64url');
if (pubBytes.length !== 32) {
throw new Error(
`Public key must be exactly 32 bytes, got ${pubBytes.length}`,
);
}
// 2. Configure HPKE cipher suite (DHKEM_X25519_HKDF_SHA256, HKDF_SHA256,
// AES_256_GCM).
const suite = new CipherSuite({
kem: new DhkemX25519HkdfSha256(),
kdf: new HkdfSha256(),
aead: new Aes256Gcm(),
});
const rawPubKey = new Uint8Array(pubBytes).buffer;
const publicKey = await suite.kem.importKey('raw', rawPubKey, true);
// 3. Encrypt the UTF-8 IP address bytes and package (enc || ciphertext).
const plaintext = new Uint8Array(Buffer.from(ipStr, 'utf8')).buffer;
const {enc, ct} = await suite.seal(
{recipientPublicKey: publicKey},
plaintext,
);
// 4. Return unpadded Base64URL-encoded payload.
return Buffer.concat([Buffer.from(enc), Buffer.from(ct)]).toString(
'base64url',
);
}
TypeScript
This example uses @hpke/core:
/**
* Encrypts an IP address using HPKE (RFC 9180).
*
* @param pubXB64Url Base64URL-encoded 32-byte X25519 public key ('x').
* @param ipStr Plaintext IPv4 or IPv6 address string (for example,
* "203.0.113.42").
* @return Unpadded Base64URL-encoded payload (enc || ciphertext).
*/
export async function encryptIp(
pubXB64Url: string,
ipStr: string,
): Promise<string> {
// 1. Decode the 32-byte X25519 public key 'x' from Base64URL.
const pubBytes = Buffer.from(pubXB64Url, 'base64url');
if (pubBytes.length !== 32) {
throw new Error(
`Public key must be exactly 32 bytes, got ${pubBytes.length}`,
);
}
// 2. Configure HPKE cipher suite (DHKEM_X25519_HKDF_SHA256, HKDF_SHA256,
// AES_256_GCM).
const suite = new CipherSuite({
kem: new DhkemX25519HkdfSha256(),
kdf: new HkdfSha256(),
aead: new Aes256Gcm(),
});
const rawPubKey = new Uint8Array(pubBytes).buffer;
const publicKey = await suite.kem.importKey('raw', rawPubKey, true);
// 3. Encrypt the UTF-8 IP address bytes and package (enc || ciphertext).
const plaintext = new Uint8Array(Buffer.from(ipStr, 'utf8')).buffer;
const {enc, ct} = await suite.seal(
{recipientPublicKey: publicKey},
plaintext,
);
// 4. Return unpadded Base64URL-encoded payload.
return Buffer.concat([Buffer.from(enc), Buffer.from(ct)]).toString(
'base64url',
);
}
Configure your HPKE library with the following cipher suite and parameters:
| Parameter | Value |
|---|---|
| Key Encapsulation Mechanism (KEM) | DHKEM(X25519, HKDF-SHA256) (0x0020) |
| Key Derivation Function (KDF) | HKDF-SHA256 (0x0001) |
| Authenticated Encryption with Associated Data (AEAD) | AES-256-GCM (0x0002) |
| Mode | Base (0x00) |
Application info (info) |
Empty ("") |
Associated data (aad) |
Empty ("") |
Format the encrypted output as the raw concatenation of the 32-byte encapsulated
ephemeral public key, enc, and the AEAD ciphertext, including the 16-byte GCM
authentication tag, with no Tink or custom header prefix. Encode the result as
an unpadded base64url string.
Encrypt each payload as a standalone single-shot HPKE message with a fresh ephemeral key pair. If your HPKE library uses a stateful sender context, create a new context for each encryption rather than reusing it across multiple messages. Encrypt the client IP address per page request, or cache the resulting base64url ciphertext string per IP address for a short time to live (TTL).
Implement the client-side workflow
After your server generates the encrypted base64url IP payload, pass the payload to GPT on the client before requesting ads.
Pass the encrypted IP to GPT
Pass the encrypted IP string to the googletag.setConfig
method using the ipAddress property on
PageSettingsConfig.
The following example inserts the server-generated encrypted IP payload
into the googletag.setConfig method before enabling services and displaying
an ad slot:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Publisher-provided IP example</title>
<script src="https://securepubads.g.doubleclick.net/tag/js/gpt.js" crossorigin="anonymous" async></script>
<script>
window.googletag = window.googletag || {cmd: []};
// Inject the base64url-encoded encrypted IP generated by your server.
const encryptedClientIp = 'ENCRYPTED_IP_PAYLOAD';
googletag.cmd.push(function() {
// Configure the encrypted IP before enabling services.
googletag.setConfig({
ipAddress: encryptedClientIp,
});
googletag
.defineSlot('/6355419/Travel/Europe/France/Paris', [300, 250], 'banner-ad')
.addService(googletag.pubads());
googletag.enableServices();
});
</script>
</head>
<body>
<div id="banner-ad" style="width: 300px; height: 250px;"></div>
<script>
googletag.cmd.push(function() {
googletag.display('banner-ad');
});
</script>
</body>
</html>Replace ENCRYPTED_IP_PAYLOAD with the unpadded
base64url-encoded string your server produces.
To clear a previously configured IP address on the page, set the ipAddress
property to the null value:
googletag.setConfig({ ipAddress: null });
Verify the integration
To verify that GPT sends your encrypted IP payload and Ad Manager decrypts the payload, use your browser's developer tools and the Google Publisher Console:
Browser developer tools
- Open your browser's developer tools.
- Select the Network tab.
- Filter for ad requests to
/gampad/ads. - Confirm that the request URL includes a non-empty
ipsquery parameter. Because GPT further encodes theipAddressvalue before sending the ad request, theipsparameter doesn't match your encrypted string verbatim.
Google Publisher Console
- To open the console, follow the steps in the Google Publisher Console guide.
- Select the Page Request tab.
Confirm that no
ipAddresswarnings appear. If there's an issue with the payload, the console logs one of the following warnings:If the value passed to the
ipAddressproperty isn't a valid base64 or base64url string between 67 and 124 characters, GPT ignores the value, omits theipsparameter from the ad request, and logs the following warning:Invalid value encountered when calling: googletag.setConfig: {"ipAddress":"..."}. (string) ... was found at PageSettingsConfig.ipAddress. Invalid IP address format.If Ad Manager can't decrypt the provided IP payload, or if the decrypted plaintext isn't a valid IPv4 or IPv6 address, the console logs the following warning and falls back to the network request IP address:
Decryption failed on the provided encrypted client IP (supplied via googletag.setConfig({ipAddress: ...})).