Skip to main content

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

What’s in the request

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. Extra fields (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’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:
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) 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 HTTP 200 with a JSON array of ad objects — one per requested placement:
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)

FastAPI

Multiple placements

Request multiple placements in one call; they share the same auction:
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: 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.
Timeout. Default 3 seconds. Raise if your stack can tolerate it; lower if you’re strict on SLAs.
Excluded topics. Per-request:

Next

Show ads

Render them in your UI.

API Reference

Full HTTP surface with all parameters.