Tutorials
Getting Started with Apache Unomi 3.1
From zero to a running CDP with multi-tenancy and API keys. Pick the path that suits you.
In this guide
Quick Start with Docker
The fastest way to get Unomi 3.1 running. You only need Docker. Full compose options (including OpenSearch) are on the Getting Started page.
Create docker-compose.yml (Elasticsearch)
version: '3.8'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:9.4.3
environment:
- discovery.type=single-node
- xpack.security.enabled=false
ports:
- 9200:9200
unomi:
image: apache/unomi:3.1.0
environment:
- UNOMI_ELASTICSEARCH_ADDRESSES=elasticsearch:9200
- UNOMI_THIRDPARTY_PROVIDER1_IPADDRESSES=0.0.0.0/0,::1,127.0.0.1
ports:
- 8181:8181
- 9443:9443
- 8102:8102
depends_on:
- elasticsearch
Start the environment
docker compose up
Verify the cluster
curl -k https://localhost:9443/cxs/cluster -u karaf:karaf
Default system credentials: karaf / karaf. Accept the self-signed certificate warning in development.
Create a Tenant
In Unomi 3.1, data lives under a tenant. Create one with the system admin user, then regenerate API keys to obtain the one-time plaintext values.
curl -X POST http://localhost:8181/cxs/tenants \
--user karaf:karaf \
-H "Content-Type: application/json" \
-d '{
"requestedId": "default",
"properties": {
"name": "Default Tenant",
"description": "Tutorial tenant"
}
}'
# Tenant create only returns maskedKey. Regenerate to get plainTextKey (shown once):
curl -X POST 'http://localhost:8181/cxs/tenants/default/apikeys?type=PUBLIC' \
--user karaf:karaf
curl -X POST 'http://localhost:8181/cxs/tenants/default/apikeys?type=PRIVATE' \
--user karaf:karaf
From each apikeys response, copy plainTextKey:
YOUR_PUBLIC_API_KEY— headerX-Unomi-Api-Keyfor/cxs/context.jsonand/cxs/eventcollectorYOUR_PRIVATE_API_KEY— basic authdefault:YOUR_PRIVATE_API_KEYfor scopes, rules, schemas, searches
karaf:karaf for context or event collector calls in native 3.1 mode.
Manual Installation
Prefer running Unomi 3.1 on your machine? Match the versions used in the project quick start.
Prerequisites
- Java 17 or later — set
JAVA_HOME. OpenJDK works fine. - Elasticsearch 9.4.3 or OpenSearch 3.x
1. Start the search engine
Elasticsearch: set cluster.name: contextElasticSearch in config/elasticsearch.yml, then bin/elasticsearch.
OpenSearch: set cluster.name: opensearch-cluster and discovery.type: single-node, then bin/opensearch. In Karaf use unomi:setup -d=unomi-distribution-opensearch -f=true before unomi:start.
2. Download & start Apache Unomi
Download the Unomi 3.1.0 binary from the download page, extract it, then:
./bin/karaf
karaf@root()> unomi:start
3. Tenant + context
Create a tenant as in Step 2, then call context with X-Unomi-Api-Key. Cluster check: https://localhost:9443/cxs/cluster (karaf/karaf).
ssh -p 8102 karaf@localhost — unomi:crud list profile, unomi:event-tail, unomi:crud list rule. Full detail: shell commands post · manual quick start.
Your First API Calls
With Unomi 3.1 running and a tenant created, explore the REST API. Replace YOUR_PUBLIC_API_KEY / YOUR_PRIVATE_API_KEY with values from Step 2.
Create a scope
Context requests and events must use a scope that already exists for the tenant.
Schema validation rejects unknown scopes (Unknown scope value in the logs):
curl -X POST http://localhost:8181/cxs/scopes \
-u "default:YOUR_PRIVATE_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{
"itemId": "my-website",
"itemType": "scope",
"metadata": {
"id": "my-website",
"name": "My Website Scope"
}
}'
Read the current context
The /cxs/context.json endpoint is the primary public endpoint. In 3.1 it requires a public API key:
curl -X POST "http://localhost:8181/cxs/context.json?sessionId=1234" \
-H "Content-Type: application/json" \
-H "X-Unomi-Api-Key: YOUR_PUBLIC_API_KEY" \
--data-raw '{
"source": {
"itemId": "homepage",
"itemType": "page",
"scope": "my-website"
},
"requiredProfileProperties": ["*"],
"requiredSessionProperties": ["*"],
"requireSegments": true,
"requireScores": true
}'
Send a custom event
Before sending a custom event, register a JSON Schema to validate it (required since Unomi 2.0):
curl -X POST http://localhost:8181/cxs/jsonSchema \
-u "default:YOUR_PRIVATE_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{
"$id": "https://unomi.apache.org/schemas/json/events/contactInfoSubmitted/1-0-0",
"$schema": "https://json-schema.org/draft/2019-09/schema",
"self": {
"vendor": "org.apache.unomi",
"name": "contactInfoSubmitted",
"format": "jsonschema",
"target": "events",
"version": "1-0-0"
},
"title": "contactInfoSubmittedEvent",
"type": "object",
"allOf": [{ "$ref": "https://unomi.apache.org/schemas/json/event/1-0-0" }],
"properties": {
"source": { "$ref": "https://unomi.apache.org/schemas/json/item/1-0-0" },
"target": { "$ref": "https://unomi.apache.org/schemas/json/item/1-0-0" },
"properties": {
"type": "object",
"properties": {
"firstName": { "type": ["null", "string"] },
"lastName": { "type": ["null", "string"] },
"email": { "type": ["null", "string"] }
}
}
},
"unevaluatedProperties": false
}'
Now send the event via the event collector (the public endpoint for submitting events):
curl -X POST http://localhost:8181/cxs/eventcollector \
-H "Content-Type: application/json" \
-H "X-Unomi-Api-Key: YOUR_PUBLIC_API_KEY" \
--data-raw '{
"sessionId": "1234",
"events": [{
"eventType": "contactInfoSubmitted",
"scope": "my-website",
"source": {
"itemType": "site",
"scope": "my-website",
"itemId": "mysite"
},
"target": {
"itemType": "form",
"scope": "my-website",
"itemId": "contactForm"
},
"properties": {
"firstName": "Jane",
"lastName": "Doe",
"email": "jane.doe@example.com"
}
}]
}'
Search events
Retrieve events matching a condition:
curl -X POST http://localhost:8181/cxs/events/search \
-u "default:YOUR_PRIVATE_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{
"offset": 0,
"limit": 20,
"sortby": "timeStamp:desc",
"condition": {
"type": "eventPropertyCondition",
"parameterValues": {
"propertyName": "properties.firstName",
"comparisonOperator": "equals",
"propertyValue": "Jane"
}
}
}'
View a profile
Use the profile UUID (from the context-profile-id cookie or event response) to look up profile details:
curl http://localhost:8181/cxs/profiles/PROFILE_UUID \
-u "default:YOUR_PRIVATE_API_KEY"
unomi:event-tail watches events in real time, unomi:crud list event / unomi:crud list profile list recent items, unomi:crud read profile PROFILE_UUID shows profile details.
Web Tracking Tutorial
Collect page views and enable personalization. Unomi 3.1 public endpoints require your tenant public API key on every context / event request.
YOUR_PUBLIC_API_KEY. Otherwise ensure requests send header X-Unomi-Api-Key (see the JavaScript tracker guide in the manual).
Add the tracker to your page
Include the tracker script and initialize it (scope must exist under your tenant):
<!-- Load the Unomi web tracker -->
<script src="/tracker/unomi-web-tracker.min.js"></script>
<script>
(function () {
var conf = {
"scope": "my-website",
"site": {
"siteInfo": { "siteID": "my-website" }
},
"page": {
"pageInfo": {
"pageID": "home",
"pageName": document.title,
"pagePath": document.location.pathname,
"destinationURL": document.location.origin + document.location.pathname,
"referringURL": document.referrer || document.location.origin + "/",
"language": "en",
"categories": [],
"tags": []
},
"attributes": {},
"consentTypes": []
},
"events:": [],
"wemInitConfig": {
"contextServerUrl": document.location.origin,
"timeoutInMilliseconds": "1500",
"contextServerCookieName": "context-profile-id",
"activateWem": true,
"trackerSessionIdCookieName": "my-website-session-id",
"trackerProfileIdCookieName": "my-website-profile-id"
// If supported by your tracker build, also set apiKey: "YOUR_PUBLIC_API_KEY"
}
};
// Unomi 3.1: ensure public endpoints receive the tenant public API key
var originalOpen = XMLHttpRequest.prototype.open;
var originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function() {
this._unomiUrl = arguments[1];
return originalOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function() {
if (this._unomiUrl && String(this._unomiUrl).indexOf("/cxs/") !== -1) {
this.setRequestHeader("X-Unomi-Api-Key", "YOUR_PUBLIC_API_KEY");
}
return originalSend.apply(this, arguments);
};
// Generate a new session if needed
if (unomiWebTracker.getCookie(conf.wemInitConfig.trackerSessionIdCookieName) == null) {
unomiWebTracker.setCookie(conf.wemInitConfig.trackerSessionIdCookieName, unomiWebTracker.generateGuid(), 1);
}
unomiWebTracker.initTracker(conf);
unomiWebTracker._registerCallback(function() {
console.log("Unomi context loaded:", unomiWebTracker.getLoadedContext());
}, 'My callback');
unomiWebTracker.startTracker();
})();
</script>
Using the tracker as an NPM package
For JavaScript/TypeScript projects, install the tracker from npm:
npm install apache-unomi-tracker
# or
yarn add apache-unomi-tracker
Then import and use it:
import { useTracker } from "apache-unomi-tracker";
const unomiWebTracker = useTracker();
// ... configure and start as shown above
Rules & Personalization
Rules let Unomi react to events in real time — updating profiles, triggering actions, or integrating with external systems.
Create a rule that counts page views
This rule increments a pageViewCount property on the visitor’s profile every time a view event is received:
curl -X POST http://localhost:8181/cxs/rules \
-u "default:YOUR_PRIVATE_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{
"metadata": {
"id": "viewEventRule",
"name": "View event rule",
"description": "Increments pageViewCount on each page view"
},
"condition": {
"type": "eventTypeCondition",
"parameterValues": {
"eventTypeId": "view"
}
},
"actions": [{
"type": "incrementPropertyAction",
"parameterValues": {
"propertyName": "pageViewCount"
}
}]
}'
After creating this rule, reload your tracked page a few times, then check the profile with unomi:crud read profile PROFILE_UUID in the SSH console to see the counter increase.
Map event data to a profile
This rule listens for the contactInfoSubmitted event (from Step 3) and copies its properties to the profile:
curl -X POST http://localhost:8181/cxs/rules \
-u "default:YOUR_PRIVATE_API_KEY" \
-H "Content-Type: application/json" \
--data-raw '{
"metadata": {
"id": "setContactInfo",
"name": "Copy contact info to profile",
"description": "Maps firstName, lastName, email from event to profile"
},
"condition": {
"type": "eventTypeCondition",
"parameterValues": {
"eventTypeId": "contactInfoSubmitted"
}
},
"actions": [
{
"type": "setPropertyAction",
"parameterValues": {
"setPropertyName": "properties(firstName)",
"setPropertyValue": "eventProperty::properties(firstName)",
"setPropertyStrategy": "alwaysSet"
}
},
{
"type": "setPropertyAction",
"parameterValues": {
"setPropertyName": "properties(lastName)",
"setPropertyValue": "eventProperty::properties(lastName)",
"setPropertyStrategy": "alwaysSet"
}
},
{
"type": "setPropertyAction",
"parameterValues": {
"setPropertyName": "properties(email)",
"setPropertyValue": "eventProperty::properties(email)",
"setPropertyStrategy": "alwaysSet"
}
}
]
}'
Personalize content based on a profile property
Using the web tracker, you can register personalization rules that show different content based on profile data. For example, show a special message after 5 page views:
<div id="variant1" style="display:none">
Welcome back! You've visited this page over 5 times.
</div>
<div id="variant2" style="display:none">
Welcome! Keep exploring to unlock personalized content.
</div>
<script>
var variants = {
"var1": { content: "variant1" },
"var2": { content: "variant2" }
};
unomiWebTracker.registerPersonalizationObject({
"id": "pageViewPersonalization",
"strategy": "matching-first",
"strategyOptions": { "fallback": "var2" },
"contents": [{
"id": "var1",
"filters": [{
"condition": {
"type": "profilePropertyCondition",
"parameterValues": {
"propertyName": "properties.pageViewCount",
"comparisonOperator": "greaterThan",
"propertyValueInteger": 5
}
}
}]
}, {
"id": "var2"
}]
}, variants, false, function (successfulFilters, selectedFilter) {
if (selectedFilter) {
document.getElementById(selectedFilter.content).style.display = '';
}
});
</script>
The personalization engine evaluates conditions server-side against the visitor’s profile and returns which variant to display, keeping your business logic private.
Next Steps
You now have a working Unomi 3.1 environment with a tenant, event tracking, profile management, rules, and personalization. Here’s where to go next.
Full Documentation
Dive deeper into segmentation, scoring, conditions, and actions in the comprehensive manual.
Read the manualREST API Reference
Explore every endpoint for profiles, events, segments, rules, and more.
Browse the APIGetting Started
Docker + OpenSearch paths, auth cheat sheet, and production notes.
Open Getting StartedIntegrations
See how organizations use Unomi with CMS, CRM, e-commerce, and other platforms.
View integrationsGet Help
Stuck? The community is friendly and responsive on the mailing list and Slack.
Join the communityDebugging Tip
Enable debug logging via the Karaf SSH console to see detailed event validation and rule execution logs:
# Connect to Karaf SSH console
ssh -p 8102 karaf@localhost
# Enable schema validation debug logs
karaf@root()> log:set DEBUG org.apache.unomi.schema.impl.SchemaServiceImpl
# Watch logs in real-time
karaf@root()> log:tail
Useful commands: unomi:event-tail, unomi:rule-tail, unomi:crud list event|rule|profile, unomi:crud read profile <ID>