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

# Contextual Ads

> Get ads matched to conversation context.

The primary ad endpoint. Send conversation messages and get back a contextually matched ad with generated creative.

## Headers

<ParamField header="Authorization" type="string" required>
  Your Gravity API key. Format: `Bearer <key>`
</ParamField>

## Body

<ParamField body="messages" type="array" required>
  Conversation history. Array of `{role, content}` message objects. The engine uses the last few messages for contextual matching.
</ParamField>

<ParamField body="sessionId" type="string" required>
  Session identifier. Used for frequency capping, experiment bucketing, and reporting. If the [Gravity pixel](/ai-platforms/pixel) is installed and you're on `@gravity-ai/api` ≥ 1.1.7, the SDK auto-forwards the pixel's `gr_sess_`-prefixed session ID (30-min idle timeout, 1-day max) via `window.gravityPixel.getSessionId()`. Pass your own `sessionId` to override if you have a better session scope.
</ParamField>

<ParamField body="placements" type="array" required>
  Ad placement configuration. 1–10 placements per request.

  | Field          | Type     | Description                                                                                                                                                                                            |
  | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
  | `placement`    | `string` | Placement type. One of `above_response`, `below_response`, `inline_response`, `left_response`, `right_response`, `search_result`, `top_page`, `bottom_page`, `center_page`, `left_page`, `right_page`. |
  | `placement_id` | `string` | Unique slot identifier on your side. Ties dashboard analytics back to the spot in your UI.                                                                                                             |
</ParamField>

<ParamField body="user" type="object">
  Optional. User context for targeting and attribution. All fields are optional on the wire — the engine accepts any shape and passes extras through.

  | Field          | Type     | Description                                                                                                                                                                                                                                                                                                                            |
  | -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `id`           | `string` | Stable per-user identifier on your side. Used for frequency capping, attribution, and identity linking. The SDK defaults this to `"anonymous"` if you don't supply one.                                                                                                                                                                |
  | `ip`           | `string` | User's IP address. Auto-populated by the SDK from the incoming request.                                                                                                                                                                                                                                                                |
  | `email_hash`   | `string` | SHA-256 of `email.strip().lower()`. Canonical email field (matches the `email_hash` used by the Index `/search` API). Passing this significantly improves attribution; see [Data quality](/introduction/concepts#data-quality). The legacy `hashed_email` alias and a raw `email` field are still accepted for backward compatibility. |
  | `hashed_phone` | `string` | SHA-256 of digits-only phone.                                                                                                                                                                                                                                                                                                          |

  Extra fields (gender, age, subscription tier, interests, etc.) are accepted and stored as request context.
</ParamField>

<ParamField body="device" type="object" required>
  End-user device signals. **`ip` and `ua` are required** — requests missing either are rejected with HTTP `400` (see [Errors](#errors)). They power fraud/bot detection plus the Device and Geography breakdowns in your dashboard. The browser SDK auto-populates them via `gravityContext()`; **server-side callers must forward the client-collected `device`** (the request reaching Gravity otherwise carries your server's UA/IP, not the end user's).

  | Field     | Type     | Required | Description                                                                                                                                   |
  | --------- | -------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
  | `ua`      | `string` | **Yes**  | End-user User-Agent. Drives fraud detection and the mobile/tablet/desktop split. Send the end user's real browser/app UA — not your server's. |
  | `ip`      | `string` | **Yes**  | End-user IP. Drives fraud detection and geo-targeting. Auto-filled by the SDK from the incoming request; set it explicitly for direct HTTP.   |
  | `country` | `string` | No       | 2-letter ISO country code. Optional override — also derived 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.                                                                                                            |

  Extra fields (`timezone`, `locale`, `browser`, `device_model`, …) are accepted and stored. Native-app/non-browser clients should send their real client UA (`CFNetwork/Darwin`, `okhttp`, `ktor-client`, …) plus the end user's IP. See [Request ads → Device signals](/ai-platforms/request-ads#device-signals).
</ParamField>

<ParamField body="relevancy" type="number">
  Optional. Minimum relevancy threshold, 0.0–1.0. When omitted, the engine falls back to the publisher baseline configured in your dashboard. Both SDKs default to `0.2`.
</ParamField>

<ParamField body="excludedTopics" type="array">
  Optional. Array of topic strings to exclude from matching (e.g. `["politics"]`).
</ParamField>

<ParamField body="testAd" type="boolean" default="false">
  Optional. When `true`, returns test creative and skips billing/metrics. The SDKs set this from the inverse of their `production` flag.
</ParamField>

<RequestExample>
  ```bash theme={null}
  curl -X POST https://server.trygravity.ai/api/v1/ad \
    -H "Authorization: Bearer <your_publisher_api_key>" \
    -H "Content-Type: application/json" \
    -d '{
      "messages": [
        {"role": "user", "content": "How do I set up a PostgreSQL database?"},
        {"role": "assistant", "content": "Here are the steps to set up PostgreSQL..."}
      ],
      "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"
      }
    }'
  ```
</RequestExample>

## 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"
  }
]
```

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

### Ad object

| Field           | Type     | Description                                                                            |
| --------------- | -------- | -------------------------------------------------------------------------------------- |
| `adText`        | `string` | Generated ad copy, contextually matched                                                |
| `title`         | `string` | Product/campaign title                                                                 |
| `brandName`     | `string` | Advertiser brand name                                                                  |
| `cta`           | `string` | Call to action text                                                                    |
| `url`           | `string` | Landing page URL                                                                       |
| `favicon`       | `string` | Brand favicon URL                                                                      |
| `clickUrl`      | `string` | Tracked click URL — use this for links                                                 |
| `impUrl`        | `string` | Impression pixel URL — fire when ad is visible                                         |
| `placement`     | `string` | Echoes back the placement type this ad filled                                          |
| `placement_id`  | `string` | Echoes back your slot correlation ID                                                   |
| `campaignId`    | `string` | Campaign identifier for the matched ad                                                 |
| `leadForm`      | `object` | Lead form configuration, when the campaign has a lead form attached                    |
| `renderer_spec` | `object` | Server-delivered ad-unit layout — see [Server-rendered ads](/sdks/server-rendered-ads) |

Null fields are omitted from the response.

### `renderer_spec`

`renderer_spec` is attached **per placement, configured on Gravity's side** — there is no request parameter to opt in or out. When your placement has a default design set up, every ad served on it carries the spec; when it doesn't, the field is absent and you render your own look (or the SDK's built-in one). To get a design configured for your placement, email [support@trygravity.ai](mailto:support@trygravity.ai) with your `placement_id`. The spec format and rendering rules are documented in [Server-rendered ads](/sdks/server-rendered-ads).

When an [experiment](/ai-platforms/experiments) is active, the ad object also includes:

| Field            | Type     | Description                                       |
| ---------------- | -------- | ------------------------------------------------- |
| `variant`        | `string` | Human-readable arm label                          |
| `experiment_id`  | `string` | Canonical experiment ID                           |
| `composition_id` | `string` | Per-render composition ID (unique per impression) |
| `renderer_key`   | `string` | Which renderer to use                             |
| `composition`    | `object` | Tokens and props for the assigned renderer        |

Experiment identity is already baked into `impUrl` and `clickUrl` — downstream analytics join automatically.

<Warning>
  Always use `clickUrl` for ad links (not `url` directly) and fire `impUrl` when the ad becomes visible. This ensures accurate tracking and billing.
</Warning>

## No ad available

When no ad matches the context (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 gracefully hide the slot in that case.

## Errors

Requests missing required fields are rejected with **HTTP `400`** and a JSON body naming each missing field. `device.ip` and `device.ua` are validated on every ad request (blank/whitespace-only values count as missing):

```json theme={null}
{
  "detail": {
    "errors": [
      "Field 'device.ip' is required",
      "Field 'device.ua' is required"
    ]
  }
}
```

Fix the request by forwarding the end user's real IP and User-Agent — see [Request ads → Forwarding device server-side](/ai-platforms/request-ads#forwarding-device-server-side).
