The pattern
Fire the ad request in parallel with your LLM call. Two reasons:- Latency budget. The user’s perceived latency is your LLM stream — the ad request should never block it.
- Fire and forget. Ad fetch can fail silently without affecting the user’s chat.
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 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 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 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 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
Thedevice 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. |
timezone, locale, browser, device_model, …) are accepted and stored.
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.Forwarding device server-side
The browser SDK’sgravityContext() 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:- email:
sha256(email.strip().lower()) - phone:
sha256(re.sub(r"\D+", "", phone))(digits only)
user.email_hash (the canonical field — it matches the email_hash used by the Index /search API) and the phone hash as user.hashed_phone. Don’t send raw email or phone over the wire.
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.What’s in the response
On a successful match the endpoint returns HTTP200 with a JSON array of ad objects — one per requested placement:
{ 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)
FastAPI
Multiple placements
Request multiple placements in one call; they share the same auction: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. |
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.
Next
Show ads
Render them in your UI.
API Reference
Full HTTP surface with all parameters.

