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

# Request ads

> Server-side ad fetching — SDK and API examples, patterns, and options.

## The pattern

Fire the ad request **in parallel** with your LLM call. Two reasons:

1. **Latency budget.** The user's perceived latency is your LLM stream — the ad request should never block it.
2. **Fire and forget.** Ad fetch can fail silently without affecting the user's chat.

<CodeGroup>
  ```ts @gravity-ai/api (SDK) theme={null}
  import { Gravity } from '@gravity-ai/api';

  const gravity = new Gravity({ production: true });

  const { ads } = await gravity.getAds(req, messages, [
    { placement: 'below_response', placement_id: 'main' }
  ]);
  ```

  ```python gravity_sdk (SDK) theme={null}
  from gravity_sdk import Gravity

  gravity = Gravity(production=True)

  result = await gravity.get_ads(request, messages, [
      {"placement": "below_response", "placement_id": "main"}
  ])
  ads = result.ads
  ```

  ```bash API (direct HTTP) theme={null}
  curl -X POST https://server.trygravity.ai/api/v1/ad \
    -H "Authorization: Bearer $GRAVITY_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "messages": [{"role":"user","content":"How do I deploy a serverless Postgres DB?"}],
      "sessionId": "sess_abc123",
      "placements": [{"placement":"below_response","placement_id":"main"}],
      "user": {"id": "user_789"},
      "device": {
        "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) ...",
        "ip": "203.0.113.42"
      }
    }'
  ```
</CodeGroup>

<Warning>
  **`device.ip` and `device.ua` are required.** Ad requests missing either field are rejected with **HTTP `400`** (`{"detail": {"errors": ["Field 'device.ip' is required", "Field 'device.ua' is required"]}}`). They power fraud/bot detection, geo, and the Device breakdown. The browser SDK collects them via `gravityContext()`; **if you call the ad API server-side (most AI platforms do), you must forward the client-collected `device` through your backend** — the request reaching Gravity otherwise carries your server's UA/IP, not the end user's. See [Device signals](#device-signals).
</Warning>

## What's in the request

| Field                       | Required                                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                      |
| --------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `messages`                  | Yes                                         | Recent conversation messages. Array of `{role, content}`. The engine uses the last few for contextual matching.                                                                                                                                                                                                                                                                                                                  |
| `sessionId`                 | **Yes**                                     | Your session identifier. Used for frequency capping and experiment bucketing. Enforced by the engine. **Free if the [Gravity pixel](/ai-platforms/pixel) is installed** — the pixel mints a `gr_sess_`-prefixed session ID (30-min idle, 1-day max) and the SDK auto-forwards it via `window.gravityPixel.getSessionId()` (requires `@gravity-ai/api` ≥ 1.1.7). Override with your own value if you have a better session scope. |
| `placements`                | Yes                                         | Array of `{placement, placement_id}`. 1–10 per request.                                                                                                                                                                                                                                                                                                                                                                          |
| `user.userId`               | **Yes** for JS SDK; Optional for Python SDK | Stable per-user identifier on your side (normalized to `id` on the wire). Used for frequency capping, attribution, and identity linking. The JS SDK's `gravityContext()` throws if missing — pass `"anonymous"` (or any constant) yourself if you don't have a real one. The Python SDK's server-side fallback defaults to `"anonymous"`.                                                                                        |
| `user.email` / `user.phone` | Optional                                    | See [User data & hashing](#user-data--hashing) below — raw values are hashed by the SDK before they leave the client.                                                                                                                                                                                                                                                                                                            |
| `device`                    | **Yes** (`ip` + `ua`)                       | End-user device signals — `ua` and `ip` are **required**; `country`, `os`, and more are optional. Requests missing `device.ip` or `device.ua` are rejected with HTTP `400`. Powers fraud/bot detection plus the Device and Geography breakdowns. Collected automatically by `gravityContext()` in the browser; **server-side callers must forward it** (see [Device signals](#device-signals) below).                            |
| `excludedTopics`            | Optional                                    | Array of topic strings to exclude (e.g. `["politics"]`).                                                                                                                                                                                                                                                                                                                                                                         |
| `relevancy`                 | Optional                                    | Minimum relevancy score 0.0–1.0. When omitted, the engine falls back to the publisher baseline configured in your dashboard; the SDKs default to `0.2`. See [Relevancy tradeoff](#relevancy-tradeoff) below.                                                                                                                                                                                                                     |
| `testAd`                    | Optional                                    | Defaults to `false`. When `true`, returns test creative and skips billing/metrics. The SDKs set this from the inverse of their `production` flag.                                                                                                                                                                                                                                                                                |

## Device signals

The `device` object is how Gravity verifies each request comes from a real device and attributes it to a location. `device.ip` and `device.ua` are **required on every ad request** — without them Gravity cannot run bot/fraud detection, so requests missing either are rejected with HTTP `400` and no ad is served.

| Field     | Type     | Required | Description                                                                                                                                                             |
| --------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ua`      | `string` | **Yes**  | End-user User-Agent. Drives fraud detection and the **Device** breakdown (mobile / tablet / desktop). Send the real browser/app UA of the end user — not your server's. |
| `ip`      | `string` | **Yes**  | End-user IP. Drives fraud detection and **Geography**. The SDK auto-fills this from the incoming request; direct HTTP callers must set it explicitly.                   |
| `country` | `string` | No       | 2-letter ISO country code. Optional override — Gravity also derives country from `ip`.                                                                                  |
| `os`      | `string` | No       | Operating system (e.g. `iOS`, `Android`, `Windows`).                                                                                                                    |
| `ifa`     | `string` | No       | Mobile advertising ID (IDFA / GAID), for app publishers.                                                                                                                |
| `id`      | `string` | No       | Your stable per-device identifier, for analytics/fraud.                                                                                                                 |

Extra fields (`timezone`, `locale`, `browser`, `device_model`, …) are accepted and stored.

```json theme={null}
"device": {
  "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15",
  "ip": "203.0.113.42",
  "country": "US",
  "os": "iOS"
}
```

<Note>
  **Native-app and non-browser clients:** if your iOS/Android app calls Gravity, send the app's real client UA (e.g. `CFNetwork/Darwin`, `okhttp`, `ktor-client` — Gravity recognizes common native-app clients as mobile) and the end user's IP. For IDE/CLI surfaces, send the client application's UA string. A pure server-to-server call must still forward the end user's `device.ip` and `device.ua` through your backend — requests carrying no end-user device signals are rejected with HTTP `400`.
</Note>

### Forwarding device server-side

The browser SDK's `gravityContext()` collects `device` on the client. Your client posts that context to your backend; your backend forwards it to Gravity. The SDK's `getAds(req, ...)` / `get_ads(request, ...)` read `gravity_context` (incl. `device`) from the request body and add the end-user `ip` from the incoming request headers automatically. If you build the ad request by hand, copy the client's `device` object into the body yourself.

## User data & hashing

Passing the user's email or phone number on the ad request materially improves attribution and reporting — particularly view-through attribution, where a conversion on an advertiser's site ties back to an ad the user saw (not just clicked).

**Raw email/phone never leaves the client.** The SDK hashes them (SHA-256, normalized form) before the request is sent:

<CodeGroup>
  ```ts @gravity-ai/api theme={null}
  import { gravityContext, hashPii } from '@gravity-ai/api';

  // SDK hashes client-side before the request leaves the browser.
  const hashed = await hashPii({ email: user.email, phone: user.phone });

  const gravity_context = gravityContext({
    sessionId: chatSession.id,
    user: { userId: user.id, ...hashed },
  });
  ```

  ```python gravity_sdk theme={null}
  from gravity_sdk import Gravity

  # The Python SDK reads `user` from the client-posted request body under
  # `gravity_context.user`. Hash on the client (see the @gravity-ai/api
  # tab) — the server-side get_ads call stays clean.
  result = await gravity.get_ads(request, messages, placements)

  # Need to hash server-side? (e.g. webhooks, non-browser clients.) Post
  # the hashed fields as part of the `gravity_context.user` payload that
  # your own ad-request endpoint forwards to get_ads. The SDK's hash_pii
  # helper normalizes + SHA-256s:
  #
  #     from gravity_sdk import hash_pii
  #     hashed = hash_pii(email=current_user.email, phone=current_user.phone)
  #     # ...merge `hashed` into the gravity_context.user dict your server
  #     # sends through to this endpoint before calling gravity.get_ads().
  ```
</CodeGroup>

If you're calling the HTTP API directly and doing the hashing yourself, match the normalization:

* **email:** `sha256(email.strip().lower())`
* **phone:** `sha256(re.sub(r"\D+", "", phone))` (digits only)

Then pass the email hash as `user.email_hash` (the canonical field — it matches the `email_hash` used by the [Index `/search` API](/gravity-index/search)) and the phone hash as `user.hashed_phone`. Don't send raw email or phone over the wire.

<Note>
  The engine still accepts the legacy `user.hashed_email` alias and a raw `user.email` field for backward compatibility, but new integrations should send `user.email_hash`.
</Note>

## What's in the response

On a successful match the endpoint returns HTTP `200` with a **JSON array** of ad objects — one per requested placement:

```json theme={null}
[
  {
    "adText": "Serverless Postgres that scales to zero. Start free.",
    "title": "Neon Serverless Postgres",
    "brandName": "Neon",
    "cta": "Try Neon Free",
    "url": "https://neon.tech",
    "favicon": "https://icons.duckduckgo.com/ip3/neon.tech.ico",
    "clickUrl": "https://api.trygravity.ai/track/click?p=...",
    "impUrl": "https://api.trygravity.ai/ack?p=...",
    "placement": "below_response",
    "placement_id": "main"
  }
]
```

The SDKs wrap this array in a convenience object — `{ ads, status, elapsed }` in JS, `AdResult(ads, status, elapsed_ms, ...)` in Python — but that envelope is SDK-only and does not appear on the wire.

**Always** use `clickUrl` (not `url`) for ad links — it routes through tracking, records the click, and 302s to the landing page. **Always** fire `impUrl` when the ad becomes visible — in the SDK, `GravityAd` does this automatically via `IntersectionObserver`.

When no ad matches (or the request is filtered as a bot, times out, or hits an unrecoverable error), the endpoint returns an **HTTP `204 No Content` with an empty body** — there is no JSON payload. Your UI should hide the slot gracefully.

## Framework examples

### Next.js (App Router)

```ts theme={null}
// app/api/chat/route.ts
import { Gravity } from '@gravity-ai/api';
const gravity = new Gravity({ production: process.env.NODE_ENV === 'production' });

export async function POST(req: Request) {
  const { messages } = await req.json();

  const adPromise = gravity.getAds(req, messages, [
    { placement: 'below_response', placement_id: 'main' }
  ]);

  const stream = new ReadableStream({
    async start(controller) {
      for await (const chunk of callLLM(messages)) {
        controller.enqueue(`data: ${JSON.stringify({ type: 'chunk', content: chunk })}\n\n`);
      }
      const { ads } = await adPromise;
      controller.enqueue(`data: ${JSON.stringify({ type: 'done', ads })}\n\n`);
      controller.close();
    }
  });

  return new Response(stream, { headers: { 'Content-Type': 'text/event-stream' } });
}
```

### FastAPI

```python theme={null}
import asyncio, json
from gravity_sdk import Gravity

gravity = Gravity(production=True)

@app.post("/api/chat")
async def chat(request: Request):
    body = await request.json()
    messages = body["messages"]

    ad_task = asyncio.create_task(
        gravity.get_ads(request, messages, [
            {"placement": "below_response", "placement_id": "main"}
        ])
    )

    async def event_stream():
        async for token in stream_your_llm(messages):
            yield f"data: {json.dumps({'type': 'chunk', 'content': token})}\n\n"
        ad_result = await ad_task
        ads = [a.to_dict() for a in ad_result.ads]
        yield f"data: {json.dumps({'type': 'done', 'ads': ads})}\n\n"

    return StreamingResponse(event_stream(), media_type="text/event-stream")
```

## Multiple placements

Request multiple placements in one call; they share the same auction:

```ts theme={null}
const { ads } = await gravity.getAds(req, messages, [
  { placement: 'below_response', placement_id: 'main' },
  { placement: 'right_response', placement_id: 'sidebar' },
  { placement: 'inline_response', placement_id: 'inline-1' },
]);

// ads[0] goes in 'main', ads[1] in 'sidebar', ads[2] in 'inline-1'
```

Each slot can return an ad or `null`. Render conditionally.

## Tuning

### Relevancy tradeoff

`relevancy` is a score from 0.0 to 1.0. Default is `0.2`. It's the **single most common misconfiguration** in Gravity integrations, so it's worth understanding the tradeoff:

| Setting                 | What happens                                                                                                                 | When to use                                                                                       |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| Lower (e.g. `0.1`)      | Looser contextual match. **Higher fill rate** — ads serve more often. Individual ads may feel less tied to the conversation. | Pages where you want to monetize every turn. New integrations where you want to see traffic flow. |
| Higher (e.g. `0.4–0.5`) | Tighter contextual match. **Lower fill rate** — many turns return no ad. When you do serve, ads feel very on-topic.          | Premium surfaces where you'd rather show nothing than show a loose match.                         |

The default `0.2` is our recommendation for most integrations — enough match quality to feel relevant, enough fill to be worth monetizing. Start there and adjust if per-slot data tells you to.

```ts theme={null}
const gravity = new Gravity({ production: true, relevancy: 0.35 });
```

**Timeout.** Default 3 seconds. Raise if your stack can tolerate it; lower if you're strict on SLAs.

```ts theme={null}
const gravity = new Gravity({ production: true, timeoutMs: 5000 });
```

**Excluded topics.** Per-request:

```ts theme={null}
await gravity.getAds(req, messages, placements, {
  excludedTopics: ['politics', 'religion']
});
```

## Next

<CardGroup cols={2}>
  <Card title="Show ads" icon="browser" href="/ai-platforms/show-ads">
    Render them in your UI.
  </Card>

  <Card title="API Reference" icon="code" href="/engine/contextual-ads">
    Full HTTP surface with all parameters.
  </Card>
</CardGroup>
