LealUp Docs
API & Webhooks

Event ingestion

Send usage events to LealUp — schema, batching, deduplication, and how they feed the health score.

Event ingestion is how LealUp knows what users are doing in your product. It feeds the Product usage dimension of the health score and triggers behavior-based playbooks.

Concepts

  • Event: something that happened (e.g., user_logged_in, report_exported, feature_X_clicked).
  • Customer: which customer the event corresponds to (required).
  • User (optional): the specific user within the customer who triggered the event.
  • Properties (optional): event metadata (report type X, amount Y, etc.).

Endpoint

POST /v1/events

Auth: scope events:write. Body: array of events, up to 1000 per batch.

Event schema

{
  "customer_id": "01HCUS...",       // required
  "user_email": "[email protected]",    // optional but strongly recommended
  "user_external_id": "u_12345",     // optional, if you prefer internal IDs
  "event_name": "report_exported",   // required, snake_case
  "timestamp": "2026-04-15T14:25:00Z", // optional, default = now
  "properties": {                    // optional
    "report_type": "quarterly",
    "format": "pdf",
    "size_mb": 2.3
  },
  "external_id": "evt_abc123"        // optional, for idempotency
}

Validations

  • customer_id — must exist in your account.
  • event_name — snake_case, max 64 chars.
  • timestamp — ISO 8601 with timezone. We accept up to 30 days in the past; older events are rejected (use a separate backfill endpoint).
  • properties — JSON object, max 10KB per event.
  • external_id — max 128 chars. If sent, a duplicate event with the same external_id + customer_id is deduplicated.

Batching

Send batches, not one-at-a-time:

POST /v1/events
Authorization: Bearer ...
Content-Type: application/json
Idempotency-Key: batch-2026-04-15-14:30

{
  "events": [
    {"customer_id": "01H...", "event_name": "login", "timestamp": "..."},
    {"customer_id": "01H...", "event_name": "page_view", "timestamp": "..."},
    ...up to 1000
  ]
}

Response:

{
  "accepted": 987,
  "rejected": 13,
  "errors": [
    {"index": 5, "code": "unknown_customer", "message": "customer_id not found"},
    {"index": 42, "code": "invalid_timestamp", "message": "timestamp in future"},
    ...
  ]
}

Valid events are processed (status 200 OK even with partial rejections). Rejected events stay in the response so you can fix them.

Rate limits

  • 60 batches/min per account (up to 1000 events each = 60k events/min at peak).
  • 1M events/day on Starter, 10M on Growth, 100M on Scale, unlimited on Enterprise.
  • Excess → 429 Too Many Requests with Retry-After.

Deduplication

Use external_id to prevent duplicates:

{
  "customer_id": "01HCUS...",
  "event_name": "purchase_completed",
  "external_id": "order_12345",    // your internal ID
  "timestamp": "...",
  "properties": {"amount_usd": 99}
}

If you resend the same external_id + customer_id, LealUp silently ignores the duplicate. Useful on backoff retries.

How events feed the health score

Frequency of use

  • Days/week with at least one event → Product usage dimension.
  • A sharp drop vs 30-day baseline → can trigger an alert.

Breadth

  • Number of distinct events (diversity) → adoption signal.
  • Customers using 2 features have less breadth than those using 10.

Active users

  • DAU / MAU per customer → horizontal adoption signal (more active users = stickier).

Events marked as "aha moments"

You can mark specific events in Admin → Health → Aha moments:

  • E.g., first_report_shared → significant positive signal.
  • E.g., billing_error_encountered → negative signal.

Practical examples

Server-side ingestion (Node.js)

import { LealUpClient } from "@lealup/sdk";

const client = new LealUpClient({ apiKey: process.env.LEALUP_KEY! });

// In your backend, after any relevant action
async function trackEvent(
  customerId: string,
  userEmail: string,
  event: string,
  properties?: Record<string, unknown>,
) {
  await client.events.ingest({
    events: [{
      customer_id: customerId,
      user_email: userEmail,
      event_name: event,
      timestamp: new Date().toISOString(),
      properties,
    }],
  });
}

Batching for high volume

If you generate >100 events/sec, don't send one at a time. Use an in-memory buffer with periodic flush:

class EventBuffer {
  private buffer: Event[] = [];
  private flushInterval = 5_000; // 5 sec

  constructor(private client: LealUpClient) {
    setInterval(() => this.flush(), this.flushInterval);
  }

  add(event: Event) {
    this.buffer.push(event);
    if (this.buffer.length >= 500) this.flush(); // flush if we fill up
  }

  async flush() {
    if (this.buffer.length === 0) return;
    const batch = this.buffer.splice(0, 1000);
    await this.client.events.ingest({ events: batch });
  }
}

Ingestion from PostHog / Mixpanel

If you already send events to another product analytics tool, don't re-ingest them. Use the native integration:

LealUp extracts only the relevant events (not all) to avoid inflating the score.

Historical backfill

To load events older than 30 days:

POST /v1/events/backfill

Same schema, accepts timestamps up to 2 years back. Rate-limited more aggressively, runs async. Useful for onboarding (load historical data once).

System events (you don't generate these)

LealUp internally generates some events that appear in the timeline but you don't need to send:

  • email_sent / email_received — from Gmail.
  • meeting_held — from Calendar.
  • ticket_opened / ticket_closed — from Zendesk/Jira.
  • health_changed — when the score crosses a threshold.
  • task_created / task_completed — from playbooks.

Don't duplicate these with manual ingestion.

What NOT to send

  • Noisy events (button_hovered, scroll). They don't contribute to health, they bloat storage.
  • Unnecessary PII — don't send names, emails, or addresses in properties unless you need them.
  • Heavy payloads — if properties is >10KB, we reject. Use IDs and join later.

Hygiene rules

  • Use snake_case in event_name and properties keys.
  • Keep a list of valid events documented in your repo (taxonomy).
  • Don't rename existing events — you lose history. If needed, create a new one (report_exported_v2).
  • Test on dev before production.

Debugging

"I don't see my events"

  • GET /v1/events?customer_id=X&from=2026-04-15 — lists recorded events.
  • Verify customer_id — silent errors if the customer doesn't exist in that account.
  • Check Admin → Data → Ingestion → Logs for recent rejections.

"Health score doesn't change"

  • The score recalculates every hour by default (config in Admin → Health).
  • If you just sent events, it can take up to 1h. Use POST /v1/customers/{id}/health/recalculate to force.

"Too many customer_id not found errors"

  • Customers are created via POST /v1/customers or CSV import, before you send events.
  • If your flow is "user signs up in your app → send event", make sure to create the customer first (provisioning pattern).

Next

  • Webhooks — receive outbound signals when things happen in LealUp.

On this page