Skip to main content

Events API

Track user events and behavior programmatically via REST API.

All endpoints on this page are relative to the base URL: https://api-eu1.joryio.com - see API Overview.

Authentication

All requests require API key authentication:

Authorization: Bearer jry_live_your_api_key_here
Content-Type: application/json

The purchase event

Revenue reporting, attribution, CLV and predictive models all read one event. If you send purchases under a different name, they are not counted - anywhere.

Event nameOrder Completed
Required propertiestotal, currency
Recommendedtotal_base, base_currency, orderId
{
"eventName": "Order Completed",
"userId": "user_123",
"properties": {
"total": 149.90,
"currency": "EUR",
"total_base": 162.35,
"base_currency": "USD",
"orderId": "1024"
}
}

Why only one name

Other platforms use other names - Placed Order (Klaviyo), purchase (GA4). We deliberately do not accept them, because accepting several names means adding them together: a store running a GA4 tag alongside our connector would fire one sale under two names and see its revenue doubled. A revenue figure that is silently 2x is far harder to notice than one that is obviously 0.

So the rule is a single published contract. If your orders are not appearing, the cause is visible immediately at onboarding - and fixable - rather than quietly wrong for months.

About the amounts

total and total_base are two different figures, not alternatives:

  • total - what the customer paid, in the currency they paid in (presentment).
  • total_base + base_currency - the same order converted to your reporting currency. Send these if you sell in more than one currency, so totals across currencies add up correctly.

Send total alone if you have a single currency. orderId is optional but recommended: it de-duplicates an order that arrives more than once (a retry, a page refresh at checkout).

If you are already sending another name

Your historical events are kept, but they are not treated as orders. Switch new orders to Order Completed, and revenue reporting starts from that point. We do not rewrite past events, so nothing is silently re-interpreted underneath you.

Track Event

Track a single user event with optional properties.

This endpoint accepts two body forms: a single event object (documented here) or a bare JSON array of event objects for batch tracking (max 500) - see Track Multiple Events (array body).

Endpoint

POST /events/track

Request Body

FieldTypeRequiredDescription
userIdstringYes*Your user identifier. *One of userId, joryioUserId, anonymousId, or userAlias is required
eventNamestringYesName of the event (max 255 chars)
propertiesobjectNoEvent properties (max 200 top-level keys, 50KB, nesting depth 5)
timestampstring or numberNoEvent timestamp (ISO 8601 string or epoch milliseconds, defaults to now)
joryioUserIdstringNoJoryio's internal user ID - the 24-hex id returned in user API responses (alternative to userId)
anonymousIdstringNoAnonymous visitor ID (alternative identifier)
userAliasobjectNo{ aliasLabel, aliasName } - alias identifier (alternative to userId)
sessionIdstringNoSession identifier
deviceIdstringNoDevice identifier
clientEventIdstringNoClient-generated event ID - used as the stored event ID, so retries with the same value are deduplicated

Example Request

curl -X POST https://api-eu1.joryio.com/events/track \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"userId": "user_123",
"eventName": "Order Completed",
"properties": {
"orderId": "order_456",
"total": 99.99,
"currency": "USD",
"items": 3,
"paymentMethod": "credit_card"
},
"timestamp": "2024-01-20T14:30:00.000Z"
}'

Response

The single-object form returns the stored event's ID:

{
"eventId": "9b2f6c1e-4a8d-4f0b-9c3d-2e1f5a6b7c8d",
"success": true
}

Notes

  • Events are processed asynchronously
  • Use consistent event naming (see Event Naming Best Practices)
  • Properties are indexed for segmentation
  • Timestamp defaults to server time if not provided

Track Multiple Events (array body)

There is no separate batch endpoint: POST /events/track accepts either a single event object or a bare JSON array of event objects (no wrapper object). The array form tracks up to 500 events in one request.

Endpoint

POST /events/track

Request Body

A JSON array (max 500 elements). Each element follows the same format as the single-object form.

Every element receives full validation. An invalid element is reported in rejected by its array index - it is never silently accepted - and the remaining valid elements are still processed.

Example Request

curl -X POST https://api-eu1.joryio.com/events/track \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '[
{
"userId": "user_123",
"eventName": "Product Viewed",
"properties": {
"productId": "prod_456",
"price": 49.99
}
},
{
"userId": "user_123",
"eventName": "Added To Cart",
"properties": {
"productId": "prod_456",
"quantity": 1
}
},
{
"userId": "user_456",
"eventName": "Page Viewed",
"properties": {
"page": "/pricing"
}
}
]'

Response

Unlike the object form (which returns { eventId, success }), the array form returns an aggregate summary:

{
"processed": 3,
"accepted": 3,
"rejected": []
}
FieldDescription
processedNumber of elements received in the request array
acceptedEvents actually recorded
rejectedPer-element failures: index (position in the request array), name (the element's event name, when present), reason

Example with one invalid element:

{
"processed": 3,
"accepted": 2,
"rejected": [
{
"index": 1,
"name": "Added To Cart",
"reason": "property hacker should not exist"
}
]
}

Limits

  • Max 500 events per request (more returns 400); an empty array also returns 400
  • Each event follows the same format as the single-object form
  • Batch processing is not atomic: valid events are recorded even when some elements are rejected - check rejected for partial failures

Query Events

Retrieve events with filtering and pagination. There is no fetch-by-event-ID endpoint - filter this query instead.

Endpoint

GET /events/query

Query Parameters

ParameterTypeDefaultDescription
userIdstring-Filter by user ID
eventNamestring-Filter by event name
startDatestring-Filter events after this date (ISO 8601)
endDatestring-Filter events before this date (ISO 8601)
limitnumber100Results per page (max 1000)
offsetnumber0Number of events to skip

Example Request

# Get all "Order Completed" events in January 2024
curl -X GET "https://api-eu1.joryio.com/events/query?eventName=Order+Completed&startDate=2024-01-01T00:00:00Z&endDate=2024-02-01T00:00:00Z&limit=100" \
-H "Authorization: Bearer jry_live_your_api_key"

Response

A bare JSON array, newest first. Event fields use snake_case (they come from the analytics store):

[
{
"event_id": "9b2f6c1e-4a8d-4f0b-9c3d-2e1f5a6b7c8d",
"user_id": "665f1e2a9b3c4d5e6f7a8b9c",
"anonymous_id": "",
"event_name": "Order Completed",
"properties": {
"orderId": "order_456",
"total": 99.99
},
"timestamp": "2024-01-20 14:30:00",
"session_id": "",
"device_id": ""
},
{
"event_id": "1c3e5a7b-9d2f-4b6c-8e0a-3f5d7b9c1e2a",
"user_id": "665f1e2a9b3c4d5e6f7a8b9d",
"anonymous_id": "",
"event_name": "Order Completed",
"properties": {
"orderId": "order_789",
"total": 149.99
},
"timestamp": "2024-01-19 10:15:00",
"session_id": "",
"device_id": ""
}
]

Event Aggregation

Get aggregated event statistics with grouping and metrics.

Endpoint

POST /events/aggregate

Request Body

FieldTypeRequiredDescription
eventNamestringYesName of the event to aggregate
startDatestringNoStart date (ISO 8601)
endDatestringNoEnd date (ISO 8601)
groupBystringNoTime grouping: hour, day, week, month (default: day)
metricsarrayNoMetrics to calculate: count, sum, avg, min, max (default: ['count'])
sumFieldstringNoName of the properties key to sum/aggregate (e.g., total)
avgFieldstringNoName of the properties key to average
eventPropertiesobjectNoFilter by event properties

Example Request

curl -X POST https://api-eu1.joryio.com/events/aggregate \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"eventName": "Order Completed",
"startDate": "2024-01-01T00:00:00Z",
"endDate": "2024-02-01T00:00:00Z",
"groupBy": "day",
"metrics": ["count", "sum"],
"sumField": "total"
}'

Response

{
"success": true,
"data": {
"eventName": "Order Completed",
"groupBy": "day",
"metrics": ["count", "sum"],
"results": [
{
"period": "2024-01-01T00:00:00Z",
"count": 45,
"sum_value": 4567.89
},
{
"period": "2024-01-02T00:00:00Z",
"count": 52,
"sum_value": 5123.45
},
{
"period": "2024-01-03T00:00:00Z",
"count": 38,
"sum_value": 3890.12
}
],
"total": 3
}
}

Supported Metrics

  • count: Total number of events
  • sum: Sum of specified field values
  • avg: Average of specified field values
  • min: Minimum value of specified field
  • max: Maximum value of specified field

Example: Revenue Analysis

# Get daily revenue from purchases
curl -X POST https://api-eu1.joryio.com/events/aggregate \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"eventName": "Order Completed",
"startDate": "2024-01-01T00:00:00Z",
"endDate": "2024-01-31T00:00:00Z",
"groupBy": "day",
"metrics": ["count", "sum", "avg"],
"sumField": "total"
}'

Example: Feature Usage by Hour

# Track feature usage patterns by hour
curl -X POST https://api-eu1.joryio.com/events/aggregate \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"eventName": "Feature Used",
"startDate": "2024-01-20T00:00:00Z",
"endDate": "2024-01-21T00:00:00Z",
"groupBy": "hour",
"metrics": ["count"]
}'

Common Events

E-commerce Events

// Product Viewed
POST /events/track
{
"userId": "user_123",
"eventName": "Product Viewed",
"properties": {
"productId": "prod_456",
"productName": "Premium Plan",
"category": "Subscription",
"price": 99.99,
"currency": "USD"
}
}

// Added To Cart
POST /events/track
{
"userId": "user_123",
"eventName": "Added To Cart",
"properties": {
"productId": "prod_456",
"quantity": 1,
"price": 99.99
}
}

// Order Completed
POST /events/track
{
"userId": "user_123",
"eventName": "Order Completed",
"properties": {
"orderId": "order_789",
"total": 249.99,
"currency": "USD",
"itemCount": 3,
"discount": 25.00,
"paymentMethod": "credit_card"
}
}

User Lifecycle Events

// Signup Completed
POST /events/track
{
"userId": "user_123",
"eventName": "Signup Completed",
"properties": {
"method": "email",
"source": "homepage_cta"
}
}

// Onboarding Completed
POST /events/track
{
"userId": "user_123",
"eventName": "Onboarding Completed",
"properties": {
"stepsCompleted": 5,
"timeSpent": "8m 30s"
}
}

// Trial Started
POST /events/track
{
"userId": "user_123",
"eventName": "Trial Started",
"properties": {
"plan": "premium",
"trialDays": 14
}
}

Engagement Events

// Feature Used
POST /events/track
{
"userId": "user_123",
"eventName": "Feature Used",
"properties": {
"featureName": "export",
"exportFormat": "csv",
"recordCount": 1500
}
}

// Page Viewed
POST /events/track
{
"userId": "user_123",
"eventName": "Page Viewed",
"properties": {
"page": "/pricing",
"category": "Marketing",
"referrer": "google"
}
}

Event Properties

Best Practices

Use descriptive property names:

Good:

{
"properties": {
"productId": "prod_123",
"productName": "Premium Plan",
"price": 99.99,
"currency": "USD"
}
}

Bad:

{
"properties": {
"pid": "prod_123",
"n": "Premium Plan",
"p": 99.99
}
}

Supported Data Types

{
"properties": {
"string": "value",
"number": 99.99,
"integer": 5,
"boolean": true,
"date": "2024-01-15T10:30:00Z",
"array": ["tag1", "tag2"],
"object": {
"nested": "value",
"deep": {
"property": "value"
}
}
}
}

Reserved Properties

Properties starting with $ are reserved for system use:

  • $app_id - App identifier
  • $app_name - App name
  • $platform - Platform (web, ios, android)
  • $session_id - Session identifier
  • $anonymous_id - Anonymous user ID

Don't use these names for custom properties.


Event Limits

Size Limits

LimitValue
Max event name length255 characters
Max identifier length (userId, anonymousId, sessionId, deviceId, clientEventId)255 characters
Max top-level property keys per event200
Max properties size (JSON)50 KB
Max property nesting depth5 levels
Max events per array-body request500

Rate Limits

The Events API has no fixed per-endpoint rate limits today - see API Overview: Rate Limiting.


Error Responses

All errors use the standard error body - see API Overview: Error Response.

400 Bad Request - Validation Failure

{
"statusCode": 400,
"message": "Bad Request Exception",
"timestamp": "2026-01-15T10:30:00.000Z",
"path": "/events/track",
"errors": [
"eventName should not be empty"
]
}

400 Bad Request - Oversized Properties

{
"statusCode": 400,
"message": "Event properties exceed maximum size of 50KB (received 63KB)",
"timestamp": "2026-01-15T10:30:00.000Z",
"path": "/events/track"
}

Best Practices

1. Batch Events When Possible

Good - Batch multiple events with an array body:

await fetch('/events/track', {
method: 'POST',
body: JSON.stringify([event1, event2, event3])
});

Bad - Individual requests:

await fetch('/events/track', { method: 'POST', body: JSON.stringify(event1) });
await fetch('/events/track', { method: 'POST', body: JSON.stringify(event2) });
await fetch('/events/track', { method: 'POST', body: JSON.stringify(event3) });

2. Use Consistent Event Naming

Follow "Object + Past Tense Verb" pattern:

Good: Product Viewed, Order Completed, Trial Started Bad: view_product, clicked, user_action_123

3. Include Timestamps

For historical events, always include accurate timestamps:

{
"userId": "user_123",
"eventName": "Order Completed",
"timestamp": "2024-01-15T10:30:00.000Z", // Actual event time
"properties": { ... }
}

4. Keep Properties Lean

Only include relevant properties:

Good:

{
"eventName": "Order Completed",
"properties": {
"orderId": "order_123",
"total": 99.99,
"currency": "USD"
}
}

Bad:

{
"eventName": "Order Completed",
"properties": {
"orderId": "order_123",
"total": 99.99,
"currency": "USD",
"userAgent": "Mozilla/5.0...", // Too much detail
"sessionData": { /* large object */ },
"cookies": [ /* array of cookies */ ]
}
}

5. Handle Errors Gracefully

Implement retry logic with exponential backoff:

async function trackWithRetry(event, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch('/events/track', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(event)
});

if (response.ok) return await response.json();

if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || Math.pow(2, i);
await sleep(retryAfter * 1000);
continue;
}

throw new Error(`HTTP ${response.status}`);
} catch (error) {
if (i === maxRetries - 1) throw error;
await sleep(Math.pow(2, i) * 1000); // 1s, 2s, 4s
}
}
}

Debugging

Enable Debug Mode (SDK)

When using the Web SDK:

import JoryioSDK from '@joryio/web-sdk';

const joryio = new JoryioSDK({
sdkKey: 'jry_sdk_web_...',
enableDebug: true // Log all events to console
});

Verify Events in Dashboard

  1. Go to Users → Find user
  2. Click Activity tab
  3. See all tracked events

Common Issues

Events not appearing:

  • Verify API key is correct
  • Check user is identified
  • Ensure event name and properties are valid
  • Check rate limits

Properties not showing:

  • Verify property names are correct
  • Check data types are supported
  • Avoid reserved property names ($prefix)

SDKs

For easier integration, use official SDKs:


Next Steps