> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trygravity.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Server-side conversions (CAPI)

> Send conversion events from your backend to Gravity for higher-fidelity attribution.

<Warning>
  **CAPI is required for conversion campaigns.** Use the pixel for browser activity and attribution context, and CAPI for backend-confirmed purchases, signups, leads, and any other event type.
</Warning>

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

```mermaid theme={null}
sequenceDiagram
    participant Browser as User's Browser
    participant Pixel as Gravity Pixel
    participant Backend as Your Backend
    participant CAPI as Gravity CAPI

    Pixel->>Browser: Captures attribution context
    Browser->>Backend: Order submitted + getCAPIData() payload
    Backend->>CAPI: POST /gateway/events (attribution + order details)
    CAPI-->>Backend: { events_processed: 1, attributed: true }
```

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:

```html theme={null}
<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:

```javascript theme={null}
// 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
  })
});
```

<Note>
  `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.
</Note>

## 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:

<Note>
  **Plaintext email is supported; the API normalizes and hashes it for you.** If you hash it yourself, trim whitespace and lowercase before SHA-256 hashing its UTF-8 bytes.
</Note>

<CodeGroup>
  ```python Python theme={null}
  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()
  ```

  ```javascript Node.js theme={null}
  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();
  }
  ```

  ```bash cURL theme={null}
  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"],
          "grclid": "grclid-example-123",
          "external_id": ["customer-456"]
        },
        "custom_data": {
          "value": 99.99,
          "currency": "USD",
          "order_id": "order-789"
        }
      }]
    }'
  ```
</CodeGroup>

<Tip>
  Include `"test_event_code": "TEST123"` to validate your payload without recording a real conversion. Remove it when you go live.
</Tip>

## Event types

| Event name | When to fire |
| - | - |
| `Purchase` | Order completed |
| `Lead` | Form submission, demo request |
| `CompleteRegistration` | Account created |
| `Subscribe` | Subscription started |
| `StartTrial` | Free trial started |
| `InitiateCheckout` | Checkout started |
| `AddToCart` | Item added to cart |
| `Contact` | Contact form or phone call |
| `Schedule` | Appointment 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()`:

| Field | Description |
| - | - |
| `grclid` | Gravity click ID |
| `grcpid` | Gravity campaign ID |
| `grpuid` | Gravity publisher ID |
| `gradid` | Gravity ad ID |
| `grsig` | Gravity attribution (UTM) signature |
| `graid` | Gravity attribution ID — resolves to the other IDs server-side |
| `visitor_id` | Visitor ID |
| `session_id` | Current session ID |
| `client_user_agent` | Browser user agent string |

Pass the entire `user_data` object through unchanged from `getCAPIData()` whenever possible.

### Device context (from pixel)

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.

### Customer identity (from your backend)

Add these server-side for richer matching:

| Field | Type | Description |
| - | - | - |
| `em` | `string[]` | 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 |

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

<Tip>
  **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.
</Tip>

## Custom data

| Field | Type | Description |
| - | - | - |
| `value` | `float` | Conversion value (e.g., order total) |
| `currency` | `string` | ISO 4217 currency code (default: `USD`) |
| `order_id` | `string` | Your order/transaction ID |
| `contents` | `object[]` | Line items: `{ id, quantity, item_price, title }` |
| `content_name` | `string` | Product or content name |
| `content_category` | `string` | Product category |
| `predicted_ltv` | `float` | Predicted customer lifetime value |

## Response

```json theme={null}
{
  "events_received": 1,
  "events_processed": 1,
  "events_errored": 0,
  "results": [
    { "event_id": "order-123", "status": "ok", "attributed": true }
  ],
  "test_mode": false
}
```

| Status | Meaning |
| - | - |
| `ok` | Event processed and stored |
| `duplicate` | Already seen (same `event_id` + `event_name` within 48 hours) |
| `test_ok` | Validated but not stored (test mode) |
| `skipped` | User opted out |
| `error` | Processing 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:

<Steps>
  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

### Troubleshooting

| Symptom | Cause | Fix |
| - | - | - |
| `401 Unauthorized` | Missing or wrong API key | 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 `grclid`) 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 |

## Next

<Card title="Pixel setup" icon="code" href="/advertisers/pixel">
  Install the Gravity pixel on your site.
</Card>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.