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.
Installation
Set GRAVITY_API_KEY in your server environment. Only dependency is httpx.
Quick start
Add a few lines to your existing streaming chat endpoint. The ad request runs in parallel with your LLM call — zero added latency.
+ import asyncio
+ 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"
- yield f"data: {json.dumps({'type': 'done'})}\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")
Works with FastAPI, Starlette, Django, and Flask.
Constructor
Gravity(*, api_key=None, api_url=None, timeout=3.0, production=False, relevancy=0.2)
| Parameter | Type | Default | Description |
|---|
api_key | str | GRAVITY_API_KEY env var | Your Gravity API key |
api_url | str | Production URL | Gravity API endpoint |
timeout | float | 3.0 | Request timeout in seconds |
production | bool | False | False returns test ads (no billing) |
relevancy | float | 0.2 | Minimum relevancy threshold, 0.0-1.0. Lower = more ads with weaker contextual matches |
The client reuses its HTTP connection pool across calls. Use async with Gravity() as g: or call await gravity.close() for explicit cleanup.
get_ads()
await gravity.get_ads(request, messages, placements, *, production=None, relevancy=None)
| Parameter | Type | Default | Description |
|---|
request | framework request | — | Your server’s request object (FastAPI, Django, Flask, etc.) |
messages | list[dict] | — | Conversation messages [{"role": ..., "content": ...}] |
placements | list[dict] | — | Ad placements, e.g. [{"placement": "chat", "placement_id": "main"}] |
production | bool | None | None | Override the constructor-level production flag for this call |
relevancy | float | None | None | Override the constructor-level relevancy threshold for this call |
Return types
@dataclass
class AdResult:
ads: list[AdResponse] # Parsed ad objects
status: int # 200, 204, 0 (error)
elapsed_ms: str # e.g. "142"
request_body: dict | None
error: str | None
@dataclass
class AdResponse:
ad_text: str
title: str | None
cta: str | None
brand_name: str | None
url: str | None
favicon: str | None
imp_url: str | None
click_url: str | None
placement: str | None
placement_id: str | None
campaign_id: str | None
# Experiment / composition fields (present when an experiment is active)
variant: str | None
experiment_id: str | None
composition_id: str | None
renderer_key: str | None
composition_mode: str | None
composition: dict | None
# Lead-form envelope (present only for lead_form ad units)
lead_form: LeadFormConfig | None
Both have .to_dict() methods that serialize to the camelCase JSON shape renderers expect.
Message handling
The SDK sends the last 2 conversational messages to the Gravity API for contextual ad matching. Only messages with recognized roles are included:
user, assistant, system, developer, model (Gemini’s alias for assistant)
Messages with other roles (e.g. tool, function, ipython) are filtered out — they typically contain structured data rather than natural language.
gravity.get_ads() never raises. On any failure, it returns AdResult(ads=[]). Safe to fire-and-forget in your stream.
PII hashing
The SDK provides hash_pii() for SHA-256 hashing of emails and phone numbers, matching the normalization used by the Gravity publisher pixel and the advertiser-side conversion pipeline.
from gravity_sdk import hash_pii
hashed = hash_pii(email=user.email, phone=user.phone)
# Include hashed PII in the gravity_context.user sent from your client:
# { "gravity_context": { "user": { "id": "u123", "hashed_email": "...", "hashed_phone": "..." } } }
Normalization rules:
- Email:
strip().lower()
- Phone: digits only (e.g.
"+1 (555) 123-4567" → "15551234567")
Returns a HashedIdentity dict with only the keys that successfully hashed (hashed_email, hashed_phone), so it’s safe to spread into a user dict.
When a campaign is configured as a lead_form ad unit, the AdResponse includes a lead_form field:
@dataclass
class LeadFormConfig:
fields: list[LeadFormField]
submit_url: str
headline: str | None
description: str | None
cta_text: str | None
success_message: str | None
privacy_text: str | None
consent_text: str | None
success_redirect_url: str | None
display: str # "inline" | "modal-center" | "modal-bottom-right"
prefill: LeadFormPrefill | None
@dataclass
class LeadFormField:
name: str
label: str
type: str # "text", "email", "tel", etc.
required: bool
placeholder: str | None
options: list[str] | None
@dataclass
class LeadFormPrefill:
email: bool
phone: bool
When present, render the form and POST the submitted values to lead_form.submit_url.
Experiments
When an experiment is active, every AdResponse carries the assigned variant, experiment_id, and composition_id. Impression and click URLs already encode the assignment — no extra tracking work.
ad = result.ads[0]
if ad.experiment_id:
# Forward to whatever renderer you use. For React, pass ad.variant to
# the <GravityAd /> component; for custom renderers, read ad.renderer_key
# and ad.composition.
...