Skip to main content

Why server-side?

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

How it works

  1. The Gravity pixel runs on your site and captures attribution context automatically
  2. On conversion, your frontend reads window.gravityPixel.getCAPIData() and sends it to your backend alongside the order
  3. Your backend calls POST /gateway/events with the pixel context plus server-side details (email, order total, line items)

Step 1 — Install the pixel

If you haven’t already, add the pixel to every page:
<script>
  !function(w,d,t,u,n,a,m){w['GravityPixelObject']=n;w[n]=w[n]||function(){
  (w[n].q=w[n].q||[]).push(arguments)},w[n].l=1*new Date();a=d.createElement(t),
  m=d.getElementsByTagName(t)[0];a.async=1;a.src=u;m.parentNode.insertBefore(a,m)
  }(window,document,'script','https://code.trygravity.ai/gr-pix.js','gravity');
  gravity('init', 'YOUR_ADVERTISER_ID');
</script>

Step 2 — Read pixel data on conversion

When the user completes a conversion (purchase, signup, etc.), call getCAPIData() and pass the result to your backend:
// On your checkout confirmation / thank-you page
var 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.

Step 3 — Send the conversion from your backend

Merge the pixel data with your order details and POST to the Gravity gateway. Pick your language:
import requests
import hashlib
import time

API_KEY = "YOUR_GRAVITY_API_KEY"  # From Settings → Organization

def 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();
}
curl -X POST "https://api.trygravity.ai/gateway/events?api_key=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "test_event_code": "TEST123",
    "data": [{
      "event_name": "Purchase",
      "event_time": 1762902353,
      "event_id": "order-test-001",
      "action_source": "website",
      "event_source_url": "https://yoursite.com/thank-you",
      "user_data": {
        "em": ["customer@example.com"],
        "ph": ["+15551234567"],
        "click_id": "your-click-id",
        "external_id": ["customer-456"]
      },
      "custom_data": {
        "value": 99.99,
        "currency": "USD",
        "order_id": "order-789"
      }
    }]
  }'
Include "test_event_code": "TEST123" to validate your payload without recording a real conversion. Remove it when you go live.

Event types

Event nameWhen to fire
PurchaseOrder completed
LeadForm submission, demo request
CompleteRegistrationAccount created
SubscribeSubscription started
StartTrialFree trial started
InitiateCheckoutCheckout started
AddToCartItem added to cart
ContactContact form or phone call
ScheduleAppointment booked
Custom event names are also accepted (e.g., "event_name": "DownloadWhitepaper").

User data fields

Attribution (from pixel)

These are automatically included when you use getCAPIData():
FieldDescription
click_idClick identifier
visitor_idVisitor ID
session_idCurrent session ID
client_user_agentBrowser user agent string

Device context (from pixel)

The client_context object is automatically included by getCAPIData(). It contains device and environment signals that improve conversion quality:
FieldDescription
timezoneUser’s timezone (e.g. America/New_York)
screenScreen dimensions and color depth
viewportBrowser viewport dimensions
platformOS platform (e.g. macOS, Windows)
connectionNetwork 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.

Customer identity (from your backend)

Add these server-side for richer matching:
FieldTypeDescription
emstring[]Email address(es) — plaintext OK, auto-hashed before storage
phstring[]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
fnstringFirst name
lnstringLast name
external_idstring[]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.

Custom data

FieldTypeDescription
valuefloatConversion value (e.g., order total)
currencystringISO 4217 currency code (default: USD)
order_idstringYour order/transaction ID
contentsobject[]Line items: { id, quantity, item_price, title }
content_namestringProduct or content name
content_categorystringProduct category
predicted_ltvfloatPredicted customer lifetime value

Response

{
  "events_received": 1,
  "events_processed": 1,
  "events_errored": 0,
  "results": [
    { "event_id": "order-123", "status": "ok", "attributed": true }
  ],
  "test_mode": false
}
StatusMeaning
okEvent processed and stored
duplicateAlready seen (same event_id + event_name within 48 hours)
test_okValidated but not stored (test mode)
skippedUser opted out
errorProcessing failed — see error field

Deduplication

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.

Authentication

Pass your API key as a query parameter or Authorization header:
?api_key=YOUR_API_KEY
or
Authorization: Bearer YOUR_API_KEY
Your API key is in Settings → Organization in the Gravity dashboard.

Batch events

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.

Verify it’s working

Three checks, in order:
1

Validate your payload with a test event

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.

Troubleshooting

SymptomCauseFix
401 UnauthorizedMissing or wrong API keyCopy the key from Settings → Organization. Send it as ?api_key= or Authorization: Bearer
Every event returns duplicateReused event_id within the 48-hour dedup windowUse a unique event_id per conversion (your order ID is ideal)
status: "ok" but attributed: false on everythingEvents lack attribution signalsExpected 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 yetThe Status tab updates live as events arriveSend a real (non-test) event, then check your request is hitting https://api.trygravity.ai/gateway/events and returning 200
Card shows Gone quietWe received events before, but none in 7+ daysYour integration likely broke in a deploy — check your backend logs for failing requests

Next

Pixel setup

Install the Gravity pixel on your site.