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 name | Order Completed |
| Required properties | total, currency |
| Recommended | total_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
| Field | Type | Required | Description |
|---|---|---|---|
userId | string | Yes* | Your user identifier. *One of userId, joryioUserId, anonymousId, or userAlias is required |
eventName | string | Yes | Name of the event (max 255 chars) |
properties | object | No | Event properties (max 200 top-level keys, 50KB, nesting depth 5) |
timestamp | string or number | No | Event timestamp (ISO 8601 string or epoch milliseconds, defaults to now) |
joryioUserId | string | No | Joryio's internal user ID - the 24-hex id returned in user API responses (alternative to userId) |
anonymousId | string | No | Anonymous visitor ID (alternative identifier) |
userAlias | object | No | { aliasLabel, aliasName } - alias identifier (alternative to userId) |
sessionId | string | No | Session identifier |
deviceId | string | No | Device identifier |
clientEventId | string | No | Client-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": []
}
| Field | Description |
|---|---|
processed | Number of elements received in the request array |
accepted | Events actually recorded |
rejected | Per-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 returns400 - 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
rejectedfor 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
| Parameter | Type | Default | Description |
|---|---|---|---|
userId | string | - | Filter by user ID |
eventName | string | - | Filter by event name |
startDate | string | - | Filter events after this date (ISO 8601) |
endDate | string | - | Filter events before this date (ISO 8601) |
limit | number | 100 | Results per page (max 1000) |
offset | number | 0 | Number 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
| Field | Type | Required | Description |
|---|---|---|---|
eventName | string | Yes | Name of the event to aggregate |
startDate | string | No | Start date (ISO 8601) |
endDate | string | No | End date (ISO 8601) |
groupBy | string | No | Time grouping: hour, day, week, month (default: day) |
metrics | array | No | Metrics to calculate: count, sum, avg, min, max (default: ['count']) |
sumField | string | No | Name of the properties key to sum/aggregate (e.g., total) |
avgField | string | No | Name of the properties key to average |
eventProperties | object | No | Filter 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
| Limit | Value |
|---|---|
| Max event name length | 255 characters |
| Max identifier length (userId, anonymousId, sessionId, deviceId, clientEventId) | 255 characters |
| Max top-level property keys per event | 200 |
| Max properties size (JSON) | 50 KB |
| Max property nesting depth | 5 levels |
| Max events per array-body request | 500 |
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
- Go to Users → Find user
- Click Activity tab
- 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:
- Web SDK: Integration Guide
- iOS SDK: Swift Package / CocoaPods
- Android SDK: Gradle
- React Native SDK: npm install @joryio/react-native-sdk