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](https://datatracker.ietf.org/doc/html/rfc9180)), 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](https://datatracker.ietf.org/doc/html/rfc8037) JSON
Web Key Set (JWKS) at the following endpoint:

[`https://www.gstatic.com/real-ip/ip_enc_key.json`](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

```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

```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

```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

```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:

```json
{
  "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)](https://datatracker.ietf.org/doc/html/rfc9180) and the decoded 32-byte X25519 public key, `x`,
retrieved in [Fetch and cache the public key](https://developers.google.com/publisher-tag/guides/unmasked-ip/publisher-provided.md.txt#fetch_public_key).

The following example encrypts an IP address string:

### Java

This example uses [Tink](https://developers.google.com/tink) (`com.google.crypto.tink:tink` 1.16.0 or
higher):

```java
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`](https://pypi.org/project/pyhpke/):

```python
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`](https://www.npmjs.com/package/@hpke/core):

```javascript
/**
 * 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`](https://www.npmjs.com/package/@hpke/core):

```typescript
/**
 * 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`](https://developers.google.com/publisher-tag/reference.md.txt#googletag.setConfig)
method using the [`ipAddress`](https://developers.google.com/publisher-tag/reference.md.txt#googletag.config.PageSettingsConfig.ipAddress) property on
[`PageSettingsConfig`](https://developers.google.com/publisher-tag/reference.md.txt#googletag.config.PageSettingsConfig).

> [!CAUTION]
> **Caution:** Changes to the `ipAddress` property only apply to ad requests made after you call the `googletag.setConfig` method. To include the encrypted IP address in the initial ad request, call the `googletag.setConfig` method before calling the methods [`googletag.enableServices`](https://developers.google.com/publisher-tag/reference.md.txt#googletag.enableServices), [`googletag.display`](https://developers.google.com/publisher-tag/reference.md.txt#googletag.display), or [`PubAdsService.refresh`](https://developers.google.com/publisher-tag/reference.md.txt#googletag.PubAdsService.refresh).

The following example inserts the server-generated encrypted IP payload
into the `googletag.setConfig` method before enabling services and displaying
an ad slot:

```html
<!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:

```javascript
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](https://developers.google.com/publisher-tag/guides/publisher-console.md.txt) 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: ...})).