Set up publisher-provided IP

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-For header or a CDN-specific client IP header. Only trust forwarding headers that your proxy infrastructure adds or overrides. When the X-Forwarded-For header 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

  1. Open your browser's developer tools.
  2. Select the Network tab.
  3. Filter for ad requests to /gampad/ads.
  4. Confirm that the request URL includes a non-empty ips query parameter. Because GPT further encodes the ipAddress value before sending the ad request, the ips parameter doesn't match your encrypted string verbatim.

Google Publisher Console

  1. To open the console, follow the steps in the Google Publisher Console guide.
  2. Select the Page Request tab.
  3. Confirm that no ipAddress warnings appear. If there's an issue with the payload, the console logs one of the following warnings:

    • If the value passed to the ipAddress property isn't a valid base64 or base64url string between 67 and 124 characters, GPT ignores the value, omits the ips parameter 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: ...})).