> ## Documentation Index
> Fetch the complete documentation index at: https://outlit-codex-platform-actions-create-update-cli.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Ingest Events

> Send tracking events to Outlit

## Endpoint

```
POST https://app.outlit.ai/api/i/v1/{publicKey}/events
```

## Path Parameters

<ParamField path="publicKey" type="string" required>
  Your organization's public key. Starts with `pk_`.
</ParamField>

## Request Body

```json theme={null}
{
  "visitorId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "source": "client",
  "events": [
    {
      "type": "pageview",
      "url": "https://example.com/pricing",
      "path": "/pricing",
      "title": "Pricing - Example",
      "referrer": "https://google.com",
      "timestamp": 1699999999999
    }
  ]
}
```

### Body Parameters

<ParamField body="visitorId" type="string">
  Unique identifier for the visitor (UUID format). Required for browser (`client`) tracking. For server-side tracking (`server` source), this can be omitted; identity or attribution is derived from `email`, `userId`, `fingerprint`, `customerId`, or billing identifiers in the events.
</ParamField>

<ParamField body="source" type="string" default="client">
  Event source. One of: `client` (browser), `server`, `integration`.
</ParamField>

<ParamField body="events" type="array" required>
  Array of event objects to ingest (max 100 per request).
</ParamField>

<ParamField body="userIdentity" type="object">
  Optional user identity for immediate resolution. Used by browser SDK when user is logged in (via `setUser()`). Allows direct identity resolution instead of anonymous visitor flow.

  | Property | Type   | Description                       |
  | -------- | ------ | --------------------------------- |
  | `email`  | string | User's email address              |
  | `userId` | string | Your system-owned user/contact ID |
  | `traits` | object | User/contact traits               |
</ParamField>

<ParamField body="customerIdentity" type="object">
  Optional customer/account attribution for the whole batch. Used by browser SDK when the current user carries account/workspace context.

  | Property         | Type   | Description                                     |
  | ---------------- | ------ | ----------------------------------------------- |
  | `customerId`     | string | Your system-owned customer/account/workspace ID |
  | `customerTraits` | object | Customer/account traits                         |
</ParamField>

## Event Types

All events share these common fields:

| Field       | Type   | Required | Description                                              |
| ----------- | ------ | -------- | -------------------------------------------------------- |
| `type`      | string | Yes      | Event type (see below)                                   |
| `timestamp` | number | No       | Unix timestamp in milliseconds. Defaults to server time. |
| `url`       | string | Yes      | Full page URL                                            |
| `path`      | string | Yes      | URL path (e.g., `/pricing`)                              |
| `referrer`  | string | No       | Referrer URL                                             |
| `utm`       | object | No       | UTM parameters (see below)                               |

### UTM Parameters

```json theme={null}
{
  "utm": {
    "source": "google",
    "medium": "cpc",
    "campaign": "spring_2024",
    "term": "pricing software",
    "content": "header_cta"
  }
}
```

***

### Pageview Event

Track a page view.

```json theme={null}
{
  "type": "pageview",
  "url": "https://example.com/pricing?plan=pro",
  "path": "/pricing",
  "title": "Pricing - Example",
  "referrer": "https://google.com/search?q=example",
  "utm": {
    "source": "google",
    "medium": "cpc",
    "campaign": "spring_2024"
  },
  "timestamp": 1699999999999
}
```

<ParamField body="type" type="string" required>
  Must be `"pageview"`.
</ParamField>

<ParamField body="title" type="string">
  Page title.
</ParamField>

***

### Custom Event

Track any custom event.

```json theme={null}
{
  "type": "custom",
  "url": "https://example.com/pricing",
  "path": "/pricing",
  "eventName": "button_clicked",
  "properties": {
    "buttonId": "cta-hero",
    "page": "/pricing",
    "variant": "A"
  },
  "timestamp": 1699999999999
}
```

<ParamField body="type" type="string" required>
  Must be `"custom"`.
</ParamField>

<ParamField body="eventName" type="string" required>
  Name of the custom event.
</ParamField>

<ParamField body="properties" type="object">
  Key-value pairs of event properties. Values can be string, number, boolean, or null.
</ParamField>

***

### Form Event

Track a form submission.

```json theme={null}
{
  "type": "form",
  "url": "https://example.com/contact",
  "path": "/contact",
  "formId": "contact-form",
  "formFields": {
    "email": "jane@example.com",
    "name": "Jane Doe",
    "company": "Acme Inc",
    "message": "I'd like to learn more..."
  },
  "timestamp": 1699999999999
}
```

<ParamField body="type" type="string" required>
  Must be `"form"`.
</ParamField>

<ParamField body="formId" type="string">
  Identifier for the form (ID attribute or name).
</ParamField>

<ParamField body="formFields" type="object">
  Key-value pairs of form field values.
</ParamField>

<Warning>
  Sensitive fields (password, credit\_card, ssn, etc.) are automatically stripped on the server. Don't send them.
</Warning>

***

### Identify Event

Identify the visitor with their email/userId.

```json theme={null}
{
  "type": "identify",
  "url": "https://example.com/signup",
  "path": "/signup",
  "email": "jane@example.com",
  "userId": "usr_12345",
  "customerId": "cust_123",
  "traits": {
    "name": "Jane Doe",
    "company": "Acme Inc",
    "plan": "enterprise",
    "role": "admin"
  },
  "customerTraits": {
    "plan": "enterprise",
    "seats": 50
  },
  "timestamp": 1699999999999
}
```

<ParamField body="type" type="string" required>
  Must be `"identify"`.
</ParamField>

<ParamField body="email" type="string">
  User's email address. At least one of `email` or `userId` required.
</ParamField>

<ParamField body="userId" type="string">
  Your system-owned user/contact ID.
</ParamField>

<ParamField body="traits" type="object">
  Properties to set on the user profile.
</ParamField>

<ParamField body="customerId" type="string">
  Your system-owned customer/account/workspace ID. Send it with an identify event when you want that external account/workspace to link to the contact resolved from email or userId.
</ParamField>

<ParamField body="customerTraits" type="object">
  Properties to set on the customer profile.
</ParamField>

***

### Engagement Event

Track active time on a page. Automatically sent by the browser SDK.

```json theme={null}
{
  "type": "engagement",
  "url": "https://example.com/features",
  "path": "/features",
  "activeTimeMs": 45000,
  "totalTimeMs": 60000,
  "sessionId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "timestamp": 1699999999999
}
```

<ParamField body="type" type="string" required>
  Must be `"engagement"`.
</ParamField>

<ParamField body="activeTimeMs" type="number" required>
  Time in milliseconds the user was actively engaged (visible tab + user interactions).
</ParamField>

<ParamField body="totalTimeMs" type="number" required>
  Total wall-clock time in milliseconds on the page.
</ParamField>

<ParamField body="sessionId" type="string" required>
  Session ID (UUID) for grouping engagement events. Resets after 30 min of inactivity or tab close.
</ParamField>

***

### Calendar Event

Track calendar booking from embedded widgets (Cal.com, Calendly).

```json theme={null}
{
  "type": "calendar",
  "url": "https://example.com/demo",
  "path": "/demo",
  "provider": "cal.com",
  "eventType": "30 Minute Demo",
  "startTime": "2024-03-15T10:00:00Z",
  "endTime": "2024-03-15T10:30:00Z",
  "duration": 30,
  "isRecurring": false,
  "timestamp": 1699999999999
}
```

<ParamField body="type" type="string" required>
  Must be `"calendar"`.
</ParamField>

<ParamField body="provider" type="string" required>
  Calendar provider: `"cal.com"`, `"calendly"`, or `"unknown"`.
</ParamField>

<ParamField body="eventType" type="string">
  Meeting type name (e.g., "30 Minute Demo").
</ParamField>

<ParamField body="startTime" type="string">
  Scheduled start time (ISO 8601 format).
</ParamField>

<ParamField body="endTime" type="string">
  Scheduled end time (ISO 8601 format).
</ParamField>

<ParamField body="duration" type="number">
  Duration in minutes.
</ParamField>

<ParamField body="isRecurring" type="boolean">
  Whether this is a recurring meeting.
</ParamField>

<ParamField body="inviteeEmail" type="string">
  Invitee's email (only available via server-side webhooks).
</ParamField>

<ParamField body="inviteeName" type="string">
  Invitee's name (only available via server-side webhooks).
</ParamField>

***

### Stage Event

Track explicit activation milestones. Engaged and Inactive are derived automatically from tracked product activity, but the ingest API continues to accept those values for older SDKs and direct API callers.

```json theme={null}
{
  "type": "stage",
  "url": "https://example.com/onboarding",
  "path": "/onboarding",
  "stage": "activated",
  "properties": {
    "flow": "onboarding",
    "step": "completed"
  },
  "timestamp": 1699999999999
}
```

<ParamField body="type" type="string" required>
  Must be `"stage"`.
</ParamField>

<ParamField body="stage" type="string" required>
  The journey stage. Use `"activated"` for new integrations. `"engaged"` and `"inactive"` remain accepted for compatibility.
</ParamField>

<ParamField body="properties" type="object">
  Additional context for the stage transition.
</ParamField>

<Info>
  The `discovered` and `signed_up` stages are automatically inferred from identify events. Only use stage events for explicit activation milestone tracking.
</Info>

***

### Billing Event

Track account billing status for custom billing systems. If Stripe is connected, billing status is handled automatically.

```json theme={null}
{
  "type": "billing",
  "url": "server://cust_123",
  "path": "/",
  "status": "paid",
  "customerId": "cust_123",
  "properties": {
    "plan": "pro",
    "amount": 99
  },
  "timestamp": 1699999999999
}
```

<ParamField body="type" type="string" required>
  Must be `"billing"`.
</ParamField>

<ParamField body="status" type="string" required>
  The billing status. One of: `"trialing"`, `"paid"`, `"churned"`.
</ParamField>

<ParamField body="customerId" type="string">
  Your system-owned customer/account/workspace ID. Public billing calls should use this identifier.
</ParamField>

<ParamField body="stripeCustomerId" type="string">
  Stripe customer identifier. Prefer `customerId` unless you are sending compatibility events that already use Stripe IDs.
</ParamField>

<ParamField body="properties" type="object">
  Additional billing context such as plan, amount, or cancellation reason.
</ParamField>

<Info>
  Billing events require `customerId` or `stripeCustomerId`. They target the account, not an individual contact.
</Info>

***

## Response

### Success Response

```json theme={null}
{
  "success": true,
  "processed": 3
}
```

<ResponseField name="success" type="boolean">
  Whether the request was successful.
</ResponseField>

<ResponseField name="processed" type="number">
  Number of events successfully processed.
</ResponseField>

<ResponseField name="errors" type="array">
  Array of processing errors (only present if some events failed).
</ResponseField>

### Partial Success Response (HTTP 207)

When some events succeed and others fail:

```json theme={null}
{
  "success": false,
  "processed": 2,
  "errors": [
    {
      "index": 2,
      "message": "Invalid event type"
    }
  ]
}
```

### Error Response

```json theme={null}
{
  "success": false,
  "processed": 0,
  "errors": [
    {
      "index": -1,
      "message": "Invalid or inactive tracking configuration"
    }
  ]
}
```

## Examples

### cURL - Browser Events

```bash theme={null}
curl -X POST "https://app.outlit.ai/api/i/v1/pk_your_key/events" \
  -H "Content-Type: application/json" \
  -d '{
    "visitorId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "source": "client",
    "events": [
      {
        "type": "pageview",
        "url": "https://example.com/pricing",
        "path": "/pricing",
        "title": "Pricing",
        "timestamp": 1699999999999
      }
    ]
  }'
```

### cURL - Server Events

```bash theme={null}
curl -X POST "https://app.outlit.ai/api/i/v1/pk_your_key/events" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "server",
    "events": [
      {
        "type": "identify",
        "url": "server://jane@acme.com",
        "path": "/",
        "email": "jane@acme.com",
        "userId": "usr_123",
        "customerId": "cust_123",
        "traits": {
          "name": "Jane Doe",
          "plan": "pro"
        },
        "customerTraits": {
          "plan": "pro"
        }
      },
      {
        "type": "custom",
        "url": "server://jane@acme.com",
        "path": "/",
        "eventName": "subscription_created",
        "email": "jane@acme.com",
        "userId": "usr_123",
        "customerId": "cust_123",
        "properties": {
          "plan": "pro",
          "amount": 99
        }
      }
    ]
  }'
```

### JavaScript (fetch)

```javascript theme={null}
const response = await fetch(
  'https://app.outlit.ai/api/i/v1/pk_your_key/events',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      visitorId: 'visitor-123',
      source: 'client',
      events: [
        {
          type: 'pageview',
          url: window.location.href,
          path: window.location.pathname,
          title: document.title,
          timestamp: Date.now()
        }
      ]
    })
  }
)

const result = await response.json()
console.log(result)
```

### Python

```python theme={null}
import requests
import time

response = requests.post(
    'https://app.outlit.ai/api/i/v1/pk_your_key/events',
    json={
        'source': 'server',
        'events': [
            {
                'type': 'identify',
                'url': 'server://jane@example.com',
                'path': '/',
                'email': 'jane@example.com',
                'customerId': 'cust_123',
                'traits': {'name': 'Jane Doe'},
                'customerTraits': {'plan': 'enterprise'},
                'timestamp': int(time.time() * 1000)
            }
        ]
    }
)

print(response.json())
```

## Server-Side Events

For server-side tracking, the `visitorId` field can be omitted. Identity is resolved from the event data, including customer-only attribution when `customerId` is provided:

* **Identify events**: Use `email` and/or `userId` fields, plus optional customer context
* **Custom events**: Include top-level `email`, `userId`, `fingerprint`, and/or `customerId` fields
* **Billing events**: Include `customerId` or `stripeCustomerId`

```json theme={null}
{
  "source": "server",
  "events": [
    {
      "type": "custom",
      "url": "server://user@example.com",
      "path": "/",
      "eventName": "payment_received",
      "email": "user@example.com",
      "userId": "usr_123",
      "customerId": "cust_123",
      "properties": {
        "amount": 99,
        "currency": "USD"
      }
    }
  ]
}
```

<Warning>
  Server events require identity or account attribution. Custom events need at least one of `email`, `userId`, `fingerprint`, or `customerId`; identify events need `email` or `userId`; billing events need `customerId` or `stripeCustomerId`.
</Warning>

## Batching Best Practices

For high-volume tracking:

1. **Batch events** - Send multiple events per request (up to 100)
2. **Use background queue** - Don't block user actions
3. **Retry with backoff** - Handle temporary failures
4. **Flush on shutdown** - Send remaining events before exit

```javascript theme={null}
// Example batching implementation
const eventQueue = []
const FLUSH_INTERVAL = 5000
const MAX_BATCH_SIZE = 100

function track(event) {
  eventQueue.push({ ...event, timestamp: Date.now() })
  
  if (eventQueue.length >= MAX_BATCH_SIZE) {
    flush()
  }
}

async function flush() {
  if (eventQueue.length === 0) return
  
  const events = eventQueue.splice(0, MAX_BATCH_SIZE)
  
  try {
    await fetch('/api/i/v1/pk_xxx/events', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        visitorId: getVisitorId(),
        source: 'client',
        events
      })
    })
  } catch (error) {
    // Re-queue failed events
    eventQueue.unshift(...events)
  }
}

setInterval(flush, FLUSH_INTERVAL)
window.addEventListener('beforeunload', flush)
```
