Skip to main content
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 — 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

1

Set up a reverse proxy

Add a route on your server (e.g. /gravity/) that forwards requests to api.trygravity.ai.
2

Rewrite tracking URLs

On your server, after calling gravity.getAds(), replace the Gravity domain in impUrl and clickUrl with your own proxy domain.
3

Log on your side

Your proxy receives every impression and click request. Log whatever you need, then forward to Gravity.

1. Set up the reverse proxy

Nginx

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)

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

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)

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

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

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)
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.

3. Log impressions and clicks

Your proxy server receives every tracking request. Here’s what each path tells you:
PathEventWhen it fires
/ack or /track/impImpressionAd became visible (IntersectionObserver, 50% threshold)
/track/clickClickUser 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

// 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:
<GravityAd
  ad={ad}
  variant="card"
  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).

Show ads

How impression and click tracking works in the SDK.

Payouts

How your tracked impressions translate to revenue.