The Gravity pixel tracks page views and auto-detects some conversions client-side. But for the highest fidelity — especially for purchases, signups, and other backend-confirmed events — sending conversions server-to-server is more reliable:
More reliable — server-side calls aren’t affected by browser extensions or network conditions
Richer data — attach customer email, phone, order details, and line items your frontend doesn’t have
Deduplication — use event_id to prevent double-counting across pixel and server events
Offline conversions — attribute phone calls, in-store purchases, and CRM events back to ads
When the user completes a conversion (purchase, signup, etc.), call getCAPIData() and pass the result to your backend:
// On your checkout confirmation / thank-you pagevar capiData = window.gravityPixel.getCAPIData();fetch('/api/complete-order', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ order_id: 'order-123', total: 99.99, currency: 'USD', email: 'customer@example.com', phone: '+15551234567', // include this if you collected a phone number gravity: capiData })});
getCAPIData() is safe to call at any time after the pixel loads. If the user arrived via a Gravity ad click, the attribution fields will be populated. If not, they return null — the conversion still records, it just won’t be attributed to a specific ad.
Merge the pixel data with your order details and POST to the Gravity gateway. Pick your language:
import requestsimport hashlibimport timeAPI_KEY = "YOUR_GRAVITY_API_KEY" # From Settings → Organizationdef hash_pii(value: str) -> str: """SHA-256 hash for PII. Or send plaintext — the API hashes it for you.""" return hashlib.sha256(value.strip().lower().encode()).hexdigest()def send_gravity_conversion(order, gravity_data): user_data = gravity_data.get("user_data", {}) response = requests.post( "https://api.trygravity.ai/gateway/events", params={"api_key": API_KEY}, json={ "data": [{ "event_name": "Purchase", "event_time": int(time.time()), "event_id": f"order-{order['id']}", "action_source": "website", "event_source_url": gravity_data.get("event_source_url", ""), "user_data": { **user_data, "em": [order["email"]], # Send the phone number whenever you have one — it materially # improves match rate. Plaintext is fine (auto-hashed), or # pre-hash with hash_pii(). Omit the field if you have no phone. **({"ph": [order["phone"]]} if order.get("phone") else {}), "external_id": [str(order["customer_id"])], }, "client_context": gravity_data.get("client_context"), "custom_data": { "value": order["total"], "currency": order.get("currency", "USD"), "order_id": str(order["id"]), "contents": [ {"id": item["sku"], "quantity": item["qty"], "item_price": item["price"]} for item in order.get("items", []) ], }, }], }, ) return response.json()
const crypto = require('crypto');const API_KEY = 'YOUR_GRAVITY_API_KEY';function hashPII(value) { return crypto.createHash('sha256').update(value.trim().toLowerCase()).digest('hex');}async function sendGravityConversion(order, gravityData) { const { user_data: userData = {}, event_source_url = '', client_context = null, } = gravityData; const response = await fetch( `https://api.trygravity.ai/gateway/events?api_key=${API_KEY}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ data: [{ event_name: 'Purchase', event_time: Math.floor(Date.now() / 1000), event_id: `order-${order.id}`, action_source: 'website', event_source_url, user_data: { ...userData, em: [order.email], // Send the phone number whenever you have one — it materially // improves match rate. Plaintext is fine (auto-hashed), or // pre-hash with hashPII(). Omit the field if you have no phone. ...(order.phone ? { ph: [order.phone] } : {}), external_id: [String(order.customerId)], }, client_context, custom_data: { value: order.total, currency: order.currency || 'USD', order_id: String(order.id), contents: order.items.map(item => ({ id: item.sku, quantity: item.qty, item_price: item.price, })), }, }], }), } ); return response.json();}
The client_context object is automatically included by getCAPIData(). It contains device and environment signals that improve conversion quality:
Field
Description
timezone
User’s timezone (e.g. America/New_York)
screen
Screen dimensions and color depth
viewport
Browser viewport dimensions
platform
OS platform (e.g. macOS, Windows)
connection
Network connection type
Pass the entire client_context object through from your frontend to your backend, then include it in the CAPI request — no manipulation is needed on your end. Gravity uses these signals to improve attribution accuracy.
Email address(es) — plaintext OK, auto-hashed before storage
ph
string[]
Phone number(s) — send this whenever you collect one. Plaintext OK (auto-hashed); the API normalizes to digits before hashing, so +1 (555) 123-4567, 15551234567, and E.164 all match
fn
string
First name
ln
string
Last name
external_id
string[]
Your system’s customer/user IDs
PII fields (em, ph, fn, ln) can be sent as plaintext — the API normalizes and SHA-256 hashes them before storage. No plaintext PII is ever persisted. If you prefer to pre-hash, send 64-character hex strings and the API will detect and accept them as-is.
Always send the phone number (ph) when you have one. Most integrations send only email, but if your checkout or lead form collects a phone number, including it materially increases match and attribution rates — the phone hash is a first-class identity signal alongside email. It costs you nothing extra: send it as plaintext and the API hashes it for you.
Events are deduplicated by event_id + event_name within a 48-hour window. Always include a unique event_id (e.g., your order ID) to prevent double-counting — especially if you fire both a pixel conversion and a server-side conversion for the same purchase.
Send up to 1,000 events per request by adding more objects to the data array. Each event is processed independently — partial failures don’t reject the batch.
Send a request with "test_event_code": "TEST123". A correct setup returns events_processed: 1 with status: "test_ok". Test events are validated end-to-end but never stored, so you can iterate safely.
2
Send a real event and check the response
Remove test_event_code and send a real conversion. You want status: "ok". Don’t worry if attributed is false — attribution depends on the user having clicked a Gravity ad. A 200 response with status: "ok" means your integration is working, attributed or not.
3
Confirm in the dashboard
Open Events Manager → Status in the Gravity dashboard. The Conversions API row shows whether we’re receiving your server-side events — it flips from No data yet to Active within a minute or two of your first real event, with a “last received” time.
Copy the key from Settings → Organization. Send it as ?api_key= or Authorization: Bearer
Every event returns duplicate
Reused event_id within the 48-hour dedup window
Use a unique event_id per conversion (your order ID is ideal)
status: "ok" but attributed: false on everything
Events lack attribution signals
Expected for users who never clicked a Gravity ad. For ad-driven conversions, pass through getCAPIData() from the pixel (it carries the click ID) and include customer identity fields (em, external_id)
Status tab still shows No data yet
The Status tab updates live as events arrive
Send a real (non-test) event, then check your request is hitting https://api.trygravity.ai/gateway/events and returning 200
Card shows Gone quiet
We received events before, but none in 7+ days
Your integration likely broke in a deploy — check your backend logs for failing requests