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

# Server-side conversions (CAPI)

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

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

<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"],
          "click_id": "your-click-id",
          "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               |
| ------------------- | ------------------------- |
| `click_id`          | Click identifier          |
| `visitor_id`        | Visitor ID                |
| `session_id`        | Current session ID        |
| `client_user_agent` | Browser 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:

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

## Next

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