> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trygravity.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Impression & click tracking proxy

> Route impression and click tracking through your own domain for first-party data visibility.

When Gravity serves an ad, the response includes two tracking URLs:

* **`impUrl`** — fired when the ad becomes visible (impression pixel)
* **`clickUrl`** — navigated to when the user clicks the ad

By default these point to `api.trygravity.ai`. A **tracking proxy** lets you route them through your own domain instead, so you can log every impression and click on your own server before forwarding to Gravity.

This is the same pass-through pattern advertisers use for [first-party conversion tracking](/advertisers/server-conversions) — applied to the publisher side.

## Why proxy tracking?

* **First-party data** — impressions and clicks hit your domain, so you can log them in your own analytics, data warehouse, or billing system.
* **Verification** — independently verify the impression and click counts Gravity reports.
* **Latency visibility** — monitor tracking endpoint performance from your own infrastructure.
* **No SDK changes required** — the SDK fires whatever URL is on `impUrl` / `clickUrl`. You just rewrite them on your server before passing the ad to your client.

## How it works

<Steps>
  <Step title="Set up a reverse proxy">
    Add a route on your server (e.g. `/gravity/`) that forwards requests to `api.trygravity.ai`.
  </Step>

  <Step title="Rewrite tracking URLs">
    On your server, after calling `gravity.getAds()`, replace the Gravity domain in `impUrl` and `clickUrl` with your own proxy domain.
  </Step>

  <Step title="Log on your side">
    Your proxy receives every impression and click request. Log whatever you need, then forward to Gravity.
  </Step>
</Steps>

```mermaid theme={null}
sequenceDiagram
    participant Browser as Browser (SDK)
    participant Proxy as Your Server
    participant Gravity as Gravity API

    Browser->>Proxy: GET /gravity/ack?p=...
    Note over Proxy: Log impression
    Proxy->>Gravity: GET /ack?p=...
    Gravity-->>Proxy: 200 OK
    Proxy-->>Browser: 200 OK

    Browser->>Proxy: GET /gravity/track/click?p=...
    Note over Proxy: Log click
    Proxy->>Gravity: GET /track/click?p=...
    Gravity-->>Proxy: 302 → landing page
    Proxy-->>Browser: 302 → landing page
```

## 1. Set up the reverse proxy

### Nginx

```nginx theme={null}
location /gravity/ {
    proxy_pass https://api.trygravity.ai/;
    proxy_set_header Host api.trygravity.ai;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_ssl_server_name on;

    # Optional: log impressions and clicks
    access_log /var/log/nginx/gravity-tracking.log;
}
```

### Next.js (middleware or API route)

```ts theme={null}
// app/api/gravity/[...path]/route.ts
import { NextRequest, NextResponse } from 'next/server';

const GRAVITY_API = 'https://api.trygravity.ai';

export async function GET(
  req: NextRequest,
  { params }: { params: Promise<{ path: string[] }> }
) {
  const { path } = await params;
  const target = `${GRAVITY_API}/${path.join('/')}?${req.nextUrl.searchParams}`;

  // Log the event on your side
  console.log(`[gravity] ${path[0]}`, {
    timestamp: new Date().toISOString(),
    ip: req.headers.get('x-forwarded-for'),
    ua: req.headers.get('user-agent'),
  });

  const res = await fetch(target, {
    redirect: 'manual',
    headers: {
      'X-Forwarded-For': req.headers.get('x-forwarded-for') || '',
      'User-Agent': req.headers.get('user-agent') || '',
    },
  });

  return new NextResponse(res.body, {
    status: res.status,
    headers: {
      'Content-Type': res.headers.get('content-type') || 'application/json',
      ...(res.headers.get('location') && { Location: res.headers.get('location')! }),
    },
  });
}
```

### Express

```ts theme={null}
import { createProxyMiddleware } from 'http-proxy-middleware';

app.use('/gravity', createProxyMiddleware({
  target: 'https://api.trygravity.ai',
  changeOrigin: true,
  pathRewrite: { '^/gravity': '' },
  on: {
    proxyReq: (proxyReq, req) => {
      // Log the event on your side
      console.log(`[gravity] ${req.method} ${req.url}`, {
        timestamp: new Date().toISOString(),
        ip: req.headers['x-forwarded-for'] || req.socket.remoteAddress,
      });
    },
  },
}));
```

### Python (FastAPI)

```python theme={null}
import httpx
from fastapi import APIRouter, Request
from fastapi.responses import Response

router = APIRouter()
GRAVITY_API = "https://api.trygravity.ai"

@router.api_route("/gravity/{path:path}", methods=["GET", "POST"])
async def gravity_proxy(request: Request, path: str):
    # Log the event on your side
    print(f"[gravity] {request.method} /{path}")

    url = f"{GRAVITY_API}/{path}?{request.url.query}"
    async with httpx.AsyncClient() as client:
        resp = await client.request(
            request.method,
            url,
            headers={
                "X-Forwarded-For": request.headers.get("x-forwarded-for", ""),
                "User-Agent": request.headers.get("user-agent", ""),
            },
            content=await request.body(),
        )
    return Response(
        content=resp.content,
        status_code=resp.status_code,
        headers=dict(resp.headers),
    )
```

## 2. Rewrite tracking URLs

After calling `gravity.getAds()` on your server, replace the Gravity domain in the ad response with your proxy path before sending to the client.

### Node / TypeScript

```ts theme={null}
const { ads } = await gravity.getAds(req, messages, placements);

const YOUR_PROXY_BASE = 'https://yourapp.com/gravity';

const rewrittenAds = ads.map(ad => ({
  ...ad,
  impUrl: ad.impUrl?.replace('https://api.trygravity.ai', YOUR_PROXY_BASE),
  clickUrl: ad.clickUrl?.replace('https://api.trygravity.ai', YOUR_PROXY_BASE),
}));

// Send rewrittenAds to the client instead of ads
```

### Python

```python theme={null}
ads = await gravity.get_ads(request, messages, placements)

PROXY_BASE = "https://yourapp.com/gravity"

for ad in ads:
    if ad.imp_url:
        ad.imp_url = ad.imp_url.replace("https://api.trygravity.ai", PROXY_BASE)
    if ad.click_url:
        ad.click_url = ad.click_url.replace("https://api.trygravity.ai", PROXY_BASE)
```

<Info>
  The encrypted `?p=` token in each URL is opaque and tamper-proof. You're only changing the domain prefix, so all billing and attribution still works exactly as before on Gravity's side.
</Info>

## 3. Log impressions and clicks

Your proxy server receives every tracking request. Here's what each path tells you:

| Path                   | Event      | When it fires                                           |
| ---------------------- | ---------- | ------------------------------------------------------- |
| `/ack` or `/track/imp` | Impression | Ad became visible (IntersectionObserver, 50% threshold) |
| `/track/click`         | Click      | User clicked the ad                                     |

The `?p=` query parameter is an encrypted payload containing the click ID, campaign ID, publisher ID, ad ID, and price. You can't decrypt it (it's encrypted with Gravity's key), but you can count requests, log timestamps, and correlate with your own session/user data from request headers.

### Example: structured logging

```ts theme={null}
// Inside your proxy handler
const event = {
  type: path.includes('click') ? 'click' : 'impression',
  timestamp: new Date().toISOString(),
  ip: req.headers['x-forwarded-for'],
  userAgent: req.headers['user-agent'],
  referer: req.headers['referer'],
};

// Send to your analytics pipeline
await yourAnalytics.track('gravity_ad_event', event);
```

## Alternative: client-side callbacks

If you don't need a full proxy, the React SDK has `onImpression` and `onClick` callbacks that fire client-side:

```tsx theme={null}
<GravityAd
  ad={ad}
  onImpression={() => {
    fetch('/api/analytics', {
      method: 'POST',
      body: JSON.stringify({
        event: 'impression',
        campaignId: ad.campaignId,
        placement: ad.placement,
        timestamp: Date.now(),
      }),
    });
  }}
  onClick={() => {
    fetch('/api/analytics', {
      method: 'POST',
      body: JSON.stringify({
        event: 'click',
        campaignId: ad.campaignId,
        placement: ad.placement,
        timestamp: Date.now(),
      }),
    });
  }}
/>
```

This is simpler but runs in the browser. The proxy approach gives you server-side visibility and works with any rendering method (not just the React SDK).

## Important notes

* **Always forward `X-Forwarded-For`** — Gravity uses the end user's IP for geo-targeting and fraud detection. Without it, all impressions appear to come from your server's IP.
* **Forward `User-Agent`** — used for device-type classification and bot filtering.
* **Don't cache** — every impression and click request must reach Gravity. Impressions are deduplicated server-side, but a cached response would silently drop billing events.
* **Click redirects** — the `/track/click` endpoint returns a `302` redirect to the advertiser's landing page. Make sure your proxy passes through the redirect response (don't follow it server-side).

<CardGroup cols={2}>
  <Card title="Show ads" icon="display" href="/ai-platforms/show-ads">
    How impression and click tracking works in the SDK.
  </Card>

  <Card title="Payouts" icon="money-bill" href="/ai-platforms/payouts">
    How your tracked impressions translate to revenue.
  </Card>
</CardGroup>
