> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trygravity.com/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.

  <Expandable title="placement object">
    <ParamField body="placement" type="string" required>
      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`.
    </ParamField>

    <ParamField body="placement_id" type="string" required>
      Unique slot identifier on your side. Ties dashboard analytics back to the spot in your UI. Must be unique **within a request** — duplicates are rejected.
    </ParamField>

    <ParamField body="max_ad_text_chars" type="number">
      Cap on generated ad-copy length for this slot, 50–5000. Overrides the placement's configured baseline.
    </ParamField>
  </Expandable>
</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.

  <Expandable title="user fields">
    <ParamField body="id" type="string">
      Stable per-user identifier on your side. Used for frequency capping, attribution, and identity linking. The JS SDK sends this from `user.userId` and defaults it to `"anonymous"` if you don't supply one.
    </ParamField>

    <ParamField body="ip" type="string">
      User's IP address. Auto-populated by the SDK from the incoming request.
    </ParamField>

    <ParamField body="email_hash" type="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.
    </ParamField>

    <ParamField body="hashed_phone" type="string">
      SHA-256 of digits-only phone.
    </ParamField>
  </Expandable>

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

  <Expandable title="device fields">
    <ParamField body="ua" type="string" required>
      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.
    </ParamField>

    <ParamField body="ip" type="string" required>
      End-user IP. Drives fraud detection and geo-targeting. Auto-filled by the SDK from the incoming request; set it explicitly for direct HTTP.
    </ParamField>

    <ParamField body="country" type="string">
      2-letter ISO country code. Optional override — also derived from `ip`.
    </ParamField>

    <ParamField body="os" type="string">
      Operating system (e.g. `iOS`, `Android`, `Windows`).
    </ParamField>

    <ParamField body="ifa" type="string">
      Mobile advertising ID (IDFA / GAID), for app publishers.
    </ParamField>

    <ParamField body="id" type="string">
      Your stable per-device identifier.
    </ParamField>
  </Expandable>

  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>

<ParamField body="testAdFormat" type="string">
  Optional, test requests only (`testAd: true`). Forces a sample format so you can build and check your UI on demand.

  | Value | Returns |
  | - | - |
  | `"image"` | A sample ad with `brandImage` and an image layout in `renderer_spec`, even if you don't use server-rendered designs. |
  | `"lead_form"` | A sample ad with a `leadForm` envelope. Submissions are discarded. |
  | `"iframe"` | A sample iframe creative (requires `supportsFrames: true`). |

  Combine with `alwaysReturn: true` to get an ad on every request. With `"image"`, pass `adLayout` (`"image_left"`, `"image_right"` or `"image_top"`) to preview a specific layout; the default is `"image_right"`. Live traffic ignores `adLayout`, since Gravity picks the layout per publisher.
</ParamField>

<ParamField body="specCapabilities" type="string">
  Optional. The server-rendered design vocabulary your client supports. Official SDKs set this automatically.

  | Capability | Meaning |
  | - | - |
  | `"psp1"` | Native iOS or Android renderer. |
  | `"web1"` | Web renderer. |

  Set `supportsSpec: false` to opt out of server-rendered designs. The legacy `supportsSpec: true` value is treated as web support.
</ParamField>

<ParamField body="consent" type="object">
  Optional. Privacy/consent signals for the end user. When omitted, behavior is unchanged from today.

  | Field | Type | Description |
  | - | - | - |
  | `gdprApplies` | `boolean` | Whether GDPR applies to this user (EU/EEA/UK). |
  | `tcfString` | `string` | IAB TCF consent string collected by your CMP. |
  | `usPrivacy` | `string` | US privacy (CCPA/CPRA) string, e.g. `"1YNN"`. |

  When `gdprApplies` is `true` and no valid `tcfString` is present, the engine serves **contextual-only** ads: it skips all processing keyed to pseudonymous user identifiers for that request (personalization, identity-keyed telemetry). Contextual matching — Gravity's core model — needs no personal data, so ads still serve.
</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 |
| `brandImage` | `string` | Product image URL, or `""` when the ad has no image — see [Image ads](#image-ads) |
| `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 |
| `renderer_spec` | `object` | Server-delivered ad-unit layout — see [Server-rendered ads](/sdks/server-rendered-ads) |
| `feedbackPrompt` | `boolean` | When `true`, the SDK shows a one-tap thumbs-up/down prompt under the ad — see [Ad feedback](/engine/ad-feedback#server-prompted-feedback). Absent on most ads. |

Null fields are omitted from the response.

### `renderer_spec`

Gravity configures server-rendered designs per placement. When a compatible design is available, the response includes `renderer_spec`; otherwise the SDK uses its built-in card. Official SDKs handle this automatically. Contact [support@trygravity.ai](mailto:support@trygravity.ai) to configure a custom placement design.

### Image ads

When the matched campaign has a product image, the ad includes `brandImage` (the image URL) and a `renderer_spec` that lays it out as an image card: the image on one side, the copy on the other, separated by a divider. Ads without an image return `brandImage: ""` and render as the normal text card.

* **Official SDKs** (`@gravity-ai/react` 1.3.0+, `@gravity-ai/ui` 0.3.0+, `@gravity-ai/react-native` 0.2.0+) render the image card automatically. Older versions ignore it and show the text card.
* **Raw API integrations** can ignore `renderer_spec` and draw their own design from `brandImage`, `title`, `adText`, `cta` and `clickUrl`.
* The layout (`image_left`, `image_right` or `image_top`) is chosen server-side per publisher. You don't need to send anything.

To get an image ad on demand while building your UI:

```json theme={null}
{ "testAd": true, "alwaysReturn": true, "testAdFormat": "image" }
```

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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.