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/eventsAuth: 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 sameexternal_id+customer_idis 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 RequestswithRetry-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:
- PostHog → see Other integrations.
- Mixpanel → in beta.
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/backfillSame 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
propertiesunless you need them. - Heavy payloads — if
propertiesis >10KB, we reject. Use IDs and join later.
Hygiene rules
- Use
snake_caseinevent_nameandpropertieskeys. - 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/recalculateto force.
"Too many customer_id not found errors"
- Customers are created via
POST /v1/customersor 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.