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

# Server-side events

> Send events from your backend via the REST API

Some actions only happen on your backend: payment confirmations, form submissions processed server-side, CRM updates, webhook callbacks. Send them to pagent as [events](/docs/guides/tracking/events) with the REST API. They are linked to the visitor's browser session, so they count in the tests that visitor saw.

Like events from the browser, server events are sent by label. No goal has to exist first. Once events arrive, build an [event goal](/docs/guides/tracking/conversion-goals) on them.

## How it works

1. **Create an API key** in pagent under **Settings → API Keys**.
2. **Capture the session** in the browser with `window._pgnt.variables()`.
3. **Pass it to your backend** with your existing request.
4. **Send the event** from your backend to `POST /v2/events`.

> **Important**: `session_data.session_id` links the event to the visitor's session and tests. Without it, the event is recorded but cannot be attributed to a test.

***

## Prerequisites

* The [pagent SDK](/docs/guides/integration/sdk-integration) installed on your website.

***

## 1. Create an API key

1. Navigate to **Settings → API Keys** in pagent.
2. Click **Create API Key**.
3. Give it a descriptive name, for example "Production Backend".
4. Copy the API key immediately.

> **Important**: The API key is displayed only once at creation. Store it securely as an environment variable. If you lose the key, delete it and create a new one. Keys expire after one year by default.

***

## 2. Capture the session in the browser

`window._pgnt.variables()` returns the identifiers of the current visitor:

```javascript theme={"dark"}
const session = window._pgnt.variables();
// {
//   session_id: "abc123…",
//   user_id: "xyz789…",
//   visit_id: "v_456…",
//   …
// }
```

Send the whole object to your backend with the request that triggers the action:

```javascript theme={"dark"}
fetch("/api/checkout", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
        cart_id: "cart_abc123",
        pagent: window._pgnt.variables()
    })
});
```

`/v2/events` accepts it as `session_data` as is. Fields it doesn't need are ignored. See [Variables](/docs/guides/tracking/variables) for everything the method returns.

***

## 3. Send events

### Single event

| | |
| - | - |
| **URL** | `https://ingest.pagent.ai/v2/events` |
| **Method** | `POST` |
| **Authorization** | `Bearer <API_KEY>` |
| **Content-Type** | `application/json` |

```json theme={"dark"}
{
    "session_data": {
        "session_id": "abc123",
        "user_id": "xyz789",
        "visit_id": "v_456"
    },
    "label": "purchase",
    "revenue": 2499,
    "properties": {
        "order_id": "ORD-1042",
        "plan": "annual"
    }
}
```

| Field | Type | Required | Description |
| - | - | - | - |
| `session_data` | object | Recommended | The output of `_pgnt.variables()`. Without it the event is not linked to a session. |
| `session_data.session_id` | string | Recommended | The browser session identifier. Required for test attribution. |
| `session_data.user_id` | string | Recommended | The visitor's persistent identifier. |
| `session_data.visit_id` | string | No | The page view identifier. |
| `label` | string | Yes | The event name. Same [label rules](/docs/guides/tracking/events#labels) as in the browser. |
| `revenue` | integer | No | Value in cents, for example `999` is \$9.99. |
| `properties` | object or array | No | Custom metadata about the event. |
| `time` | number | No | Unix timestamp in milliseconds. Defaults to the time pagent receives it. |

### Bulk

Send up to 100 events in one request to `POST https://ingest.pagent.ai/v2/events/bulk`. Events can belong to different visitors, which suits webhooks and background jobs.

```json theme={"dark"}
{
    "events": [
        {
            "session_data": { "session_id": "sess_001", "user_id": "user_a" },
            "label": "purchase",
            "revenue": 2499
        },
        {
            "session_data": { "session_id": "sess_002", "user_id": "user_b" },
            "label": "purchase",
            "revenue": 4999
        }
    ]
}
```

### Responses

| Status | Body | Description |
| - | - | - |
| `200` | `{ "accepted": 2, "rejected": [] }` | Events accepted. The bulk endpoint lists events it skipped under `rejected`. |
| `400` | `{ "error": "..." }` | The request body is invalid. |
| `401` | `{ "error": "..." }` | The API key is missing, invalid or expired. |
| `422` | `{ "accepted": 0, "rejected": [{ "label": "...", "reason": "..." }] }` | No event was accepted. |
| `429` | | Rate limit exceeded. |

Rejection reasons are `empty` and `invalid_charset` (the label breaks the label rules) and `label_limit_reached` (the website already has 200 distinct event labels).

### Code examples

#### cURL

```bash theme={"dark"}
curl -X POST https://ingest.pagent.ai/v2/events \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "session_data": {
      "session_id": "abc123",
      "user_id": "xyz789",
      "visit_id": "v_456"
    },
    "label": "purchase",
    "revenue": 2499
  }'
```

#### Node.js

```javascript theme={"dark"}
const PAGENT_API_KEY = process.env.PAGENT_API_KEY;

async function sendPagentEvent(sessionData, { label, revenue, properties } = {}) {
    const response = await fetch("https://ingest.pagent.ai/v2/events", {
        method: "POST",
        headers: {
            "Authorization": `Bearer ${PAGENT_API_KEY}`,
            "Content-Type": "application/json"
        },
        body: JSON.stringify({ session_data: sessionData, label, revenue, properties })
    });

    if (!response.ok) {
        const body = await response.json().catch(() => ({}));
        throw new Error(`pagent API error (${response.status}): ${JSON.stringify(body)}`);
    }

    return response.json(); // { accepted: 1 }
}

// Usage in an Express route handler
app.post("/api/checkout", async (req, res) => {
    const { pagent, cart_total } = req.body; // pagent = _pgnt.variables() from the browser

    const order = await processCheckout(req.body);

    await sendPagentEvent(pagent, {
        label: "purchase",
        revenue: Math.round(cart_total * 100), // dollars to cents
        properties: { order_id: order.id }
    });

    res.json({ order_id: order.id });
});
```

#### Python

```python theme={"dark"}
import os
import requests

PAGENT_API_KEY = os.environ["PAGENT_API_KEY"]

def send_pagent_event(session_data, label, revenue=None, properties=None):
    response = requests.post(
        "https://ingest.pagent.ai/v2/events",
        headers={
            "Authorization": f"Bearer {PAGENT_API_KEY}",
            "Content-Type": "application/json",
        },
        json={
            "session_data": session_data,
            "label": label,
            "revenue": revenue,
            "properties": properties,
        },
    )
    response.raise_for_status()
    return response.json()  # {"accepted": 1}
```

Server events appear under **Tracking → Sources → Events** with the source **Server**.

***

## Revenue

Revenue follows the same format as in the browser: integers in **cents**, not dollars. See [Tracking revenue](/docs/guides/tracking/revenue).

* Correct: `"revenue": 999` is \$9.99
* Incorrect: `"revenue": 9.99` is rejected with `400`

***

## Best practices

1. **Store API keys securely.** Use environment variables and never commit keys to source control.
2. **Send the real time.** If the event is sent later than it happened, set `time` to when it actually happened.
3. **Check the session.** Make sure `session_id` is not empty before you rely on the event for a test.
4. **Batch when possible.** Group events into one bulk request to reduce overhead.
5. **Retry carefully.** Retry `500` responses. Do not retry `400`, `401` or `422`.
6. **Send cents.** Convert amounts to integer cents before sending.

***

## Troubleshooting

### Events not appearing

* Check the response: a `422` lists the rejected labels and why.
* Check the label follows the [label rules](/docs/guides/tracking/events#labels).
* Confirm the API key has not expired.

### Events appear but don't count in a test

* Make sure `session_data.session_id` comes from `_pgnt.variables()` in the visitor's browser.
* Make sure an [event goal](/docs/guides/tracking/conversion-goals) on the label is added to the test.

### 401 Unauthorized

* Confirm the `Authorization: Bearer <key>` header is present and correctly formatted.
* The API key may be expired or deleted. Create a new one under **Settings → API Keys**.
* Make sure there are no extra spaces or newlines in the key value.

### Need help?

Reach out at [support@pagent.ai](mailto:support@pagent.ai).

***

## Deprecated: Conversions API

<Warning>
  `/v1/conversions`, `/v2/conversions` and `/v2/conversions/bulk` are deprecated. They keep working, but they only count for **Programmatic goals** and need the goal's `conversion_id` from `_pgnt.variables("goal_label")`. For new integrations use `/v2/events` and an event goal.

  To migrate, send the same label and revenue to `/v2/events` instead, and follow [Migrating from programmatic goals](/docs/guides/tracking/conversion-goals#migrating-from-programmatic-goals). The request bodies are almost identical: `/v2/events` takes the same `session_data`, `label`, `revenue`, `properties` and `time` fields and doesn't need `conversion_id`.
</Warning>

The reference below is kept for existing integrations. Both use the same API key as `/v2/events`.

### Capturing the goal ID

The Conversions API needs the UUID of a Programmatic goal. Pass the goal label to `variables()`:

```javascript theme={"dark"}
const vars = window._pgnt.variables("purchase");
// { session_id: "abc123…", user_id: "xyz789…", visit_id: "v_456…", conversion_id: "goal-uuid-here" }
```

`conversion_id` is `null` when the label matches no Programmatic goal published to your site.

### v1 API

#### Endpoint

| | |
| - | - |
| **URL** | `https://ingest.pagent.ai/v1/conversions` |
| **Method** | `POST` |
| **Authorization** | `Bearer <API_KEY>` |
| **Content-Type** | `application/json` |

#### Request Body

The request body contains an `events` array with 1 to 100 conversion events:

```json theme={"dark"}
{
    "events": [
        {
            "session_id": "abc123",
            "user_id": "xyz789",
            "conversion_id": "goal-uuid-here",
            "label": "purchase",
            "revenue": 2499
        }
    ]
}
```

| Field | Type | Required | Description |
| - | - | - | - |
| `session_id` | string | Yes | From `_pgnt.variables()` |
| `user_id` | string | Yes | From `_pgnt.variables()` |
| `conversion_id` | string | Yes | Goal UUID from `_pgnt.variables()` |
| `label` | string | No | Human-readable goal tag (useful for debugging) |
| `revenue` | integer | No | Value in cents (e.g., `999` = \$9.99) |
| `properties` | object or array | No | Custom metadata about the conversion |
| `time` | number | No | Unix timestamp in milliseconds (defaults to current time) |

#### Response

| Status | Body | Description |
| - | - | - |
| `200` | `{ "accepted": 1 }` | Events accepted successfully |
| `400` | `{ "error": "..." }` | Invalid request body |
| `401` | `{ "error": "..." }` | Missing, invalid, or expired API key |
| `429` | | Rate limit exceeded |

#### Code Examples

##### cURL

```bash theme={"dark"}
curl -X POST https://ingest.pagent.ai/v1/conversions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [{
      "session_id": "abc123",
      "user_id": "xyz789",
      "conversion_id": "goal-uuid-here",
      "label": "purchase",
      "revenue": 2499
    }]
  }'
```

##### Node.js

```javascript theme={"dark"}
const PAGENT_API_KEY = process.env.PAGENT_API_KEY;

async function trackConversion({ session_id, user_id, conversion_id, label, revenue }) {
    const response = await fetch("https://ingest.pagent.ai/v1/conversions", {
        method: "POST",
        headers: {
            "Authorization": `Bearer ${PAGENT_API_KEY}`,
            "Content-Type": "application/json"
        },
        body: JSON.stringify({
            events: [{ session_id, user_id, conversion_id, label, revenue }]
        })
    });

    if (!response.ok) {
        const body = await response.json();
        throw new Error(`Pagent API error (${response.status}): ${body.error}`);
    }

    return response.json(); // { accepted: 1 }
}

// Usage in an Express route handler
app.post("/api/checkout", async (req, res) => {
    const { session_id, user_id, conversion_id, cart_total } = req.body;

    // Process the checkout...
    const order = await processCheckout(req.body);

    // Track the conversion
    await trackConversion({
        session_id,
        user_id,
        conversion_id,
        label: "purchase",
        revenue: Math.round(cart_total * 100) // Convert dollars to cents
    });

    res.json({ order_id: order.id });
});
```

##### Python

```python theme={"dark"}
import os
import requests

PAGENT_API_KEY = os.environ["PAGENT_API_KEY"]

def track_conversion(session_id, user_id, conversion_id, label=None, revenue=None):
    response = requests.post(
        "https://ingest.pagent.ai/v1/conversions",
        headers={
            "Authorization": f"Bearer {PAGENT_API_KEY}",
            "Content-Type": "application/json",
        },
        json={
            "events": [{
                "session_id": session_id,
                "user_id": user_id,
                "conversion_id": conversion_id,
                "label": label,
                "revenue": revenue,
            }]
        },
    )
    response.raise_for_status()
    return response.json()  # {"accepted": 1}
```

#### Batch Events

You can send up to 100 events in a single request. Events can belong to different sessions and users, making this ideal for processing webhooks or background jobs:

```json theme={"dark"}
{
    "events": [
        {
            "session_id": "sess_001",
            "user_id": "user_a",
            "conversion_id": "goal-uuid",
            "label": "purchase",
            "revenue": 2499
        },
        {
            "session_id": "sess_002",
            "user_id": "user_b",
            "conversion_id": "goal-uuid",
            "label": "purchase",
            "revenue": 4999
        }
    ]
}
```

#### Custom Properties

Attach custom metadata to your conversion events for additional context. The `properties` field accepts an object or an array of objects.

```json theme={"dark"}
{
    "events": [{
        "session_id": "abc123",
        "user_id": "xyz789",
        "conversion_id": "goal-uuid",
        "label": "purchase",
        "revenue": 2499,
        "properties": {
            "product_id": "prod_123",
            "category": "electronics",
            "payment_method": "credit_card"
        }
    }]
}
```

### v2 API

v2 is useful when your integration cannot destructure `_pgnt.variables()` before sending: the session identifiers are grouped in a `session_data` object that you can pass through directly.

#### Single Event Endpoint

| | |
| - | - |
| **URL** | `https://ingest.pagent.ai/v2/conversions` |
| **Method** | `POST` |
| **Authorization** | `Bearer <API_KEY>` |
| **Content-Type** | `application/json` |

##### Request Body

```json theme={"dark"}
{
    "session_data": {
        "session_id": "abc123",
        "user_id": "xyz789",
        "visit_id": "v_456",
        "conversion_id": "goal-uuid-here"
    },
    "label": "purchase",
    "revenue": 2499
}
```

| Field | Type | Required | Description |
| - | - | - | - |
| `session_data` | object | Yes | Session identifiers from `_pgnt.variables()` |
| `session_data.session_id` | string | Yes | The current browser session identifier |
| `session_data.user_id` | string | Yes | The visitor's persistent user identifier |
| `session_data.visit_id` | string | No | The current page view identifier |
| `session_data.conversion_id` | string | Yes | Goal UUID from `_pgnt.variables()` |
| `label` | string | No | Human-readable goal tag (useful for debugging) |
| `revenue` | integer | No | Value in cents (e.g., `999` = \$9.99) |
| `properties` | object or array | No | Custom metadata about the conversion |
| `time` | number | No | Unix timestamp in milliseconds (defaults to current time) |

##### Response

| Status | Body | Description |
| - | - | - |
| `200` | `{ "accepted": 1 }` | Event accepted successfully |
| `400` | `{ "error": "..." }` | Invalid request body |
| `401` | `{ "error": "..." }` | Missing, invalid, or expired API key |
| `429` | | Rate limit exceeded |

#### Bulk Endpoint

| | |
| - | - |
| **URL** | `https://ingest.pagent.ai/v2/conversions/bulk` |
| **Method** | `POST` |
| **Authorization** | `Bearer <API_KEY>` |
| **Content-Type** | `application/json` |

Send up to 100 events in a single request. Each event uses the same `session_data` structure as the single-event endpoint:

```json theme={"dark"}
{
    "events": [
        {
            "session_data": {
                "session_id": "sess_001",
                "user_id": "user_a",
                "conversion_id": "goal-uuid"
            },
            "label": "purchase",
            "revenue": 2499
        },
        {
            "session_data": {
                "session_id": "sess_002",
                "user_id": "user_b",
                "conversion_id": "goal-uuid"
            },
            "revenue": 4999
        }
    ]
}
```

#### Code Examples

##### cURL

```bash theme={"dark"}
curl -X POST https://ingest.pagent.ai/v2/conversions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "session_data": {
      "session_id": "abc123",
      "user_id": "xyz789",
      "visit_id": "v_456",
      "conversion_id": "goal-uuid-here"
    },
    "label": "purchase",
    "revenue": 2499
  }'
```

##### Node.js

```javascript theme={"dark"}
const PAGENT_API_KEY = process.env.PAGENT_API_KEY;

async function trackConversion(sessionData, { label, revenue } = {}) {
    const response = await fetch("https://ingest.pagent.ai/v2/conversions", {
        method: "POST",
        headers: {
            "Authorization": `Bearer ${PAGENT_API_KEY}`,
            "Content-Type": "application/json"
        },
        body: JSON.stringify({
            session_data: sessionData,
            label,
            revenue
        })
    });

    if (!response.ok) {
        const body = await response.json();
        throw new Error(`Pagent API error (${response.status}): ${body.error}`);
    }

    return response.json(); // { accepted: 1 }
}

// Usage in an Express route handler
app.post("/api/checkout", async (req, res) => {
    const { pagent, cart_total } = req.body;
    // pagent is the raw _pgnt.variables() output passed from the frontend

    // Process the checkout...
    const order = await processCheckout(req.body);

    // Track the conversion: pass session_data directly, no destructuring needed
    await trackConversion(pagent, {
        label: "purchase",
        revenue: Math.round(cart_total * 100) // Convert dollars to cents
    });

    res.json({ order_id: order.id });
});
```

##### Python

```python theme={"dark"}
import os
import requests

PAGENT_API_KEY = os.environ["PAGENT_API_KEY"]

def track_conversion(session_data, label=None, revenue=None):
    response = requests.post(
        "https://ingest.pagent.ai/v2/conversions",
        headers={
            "Authorization": f"Bearer {PAGENT_API_KEY}",
            "Content-Type": "application/json",
        },
        json={
            "session_data": session_data,
            "label": label,
            "revenue": revenue,
        },
    )
    response.raise_for_status()
    return response.json()  # {"accepted": 1}
```

#### Custom Properties

Attach custom metadata using the `properties` field (accepts an object or an array of objects):

```json theme={"dark"}
{
    "session_data": {
        "session_id": "abc123",
        "user_id": "xyz789",
        "conversion_id": "goal-uuid"
    },
    "label": "purchase",
    "revenue": 2499,
    "properties": {
        "product_id": "prod_123",
        "category": "electronics",
        "payment_method": "credit_card"
    }
}
```
