Analytics
External integrationThe Analytics service provides two APIs: one for ingesting events from your integration or from the platform itself, and one for querying those events with aggregations, time bucketing, and filtering. The service runs independently of the Core API to isolate analytical query load from transactional operations.
/api/analytics/. They route through the same API Gateway as all other Altafid endpoints and require the same JWT authentication headers.How it works: The Core API automatically publishes platform events (contact created, login, export completed, etc.) to the Analytics service via an internal event outbox. You can also send your own custom events from your integration using the event ingestion endpoint.
Event ingestion
POST/api/analytics/events
Send one or more structured events to the analytics store. The endpoint returns 202 Accepted — events are validated then written asynchronously. Use this endpoint to track actions in your integration, custom workflow milestones, or to replay historical events during initial data migration.
Request body
The request body is an AnalyticsEvent object:
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "contact.created",
"occurredAt": "2026-04-22T10:30:00Z",
"tenantId": "T019A16FF372A70B5A9307B00CE85E4DA",
"actorType": "TENANT_STAFF",
"actorId": "TS019C06688AA57248B9EA5325DBDFC48E",
"entityType": "CONTACT",
"entityId": "C019B9D0FD0E5772F9E8ED189895D40C4",
"schemaVersion": 1,
"attributes": {
"contactType": "ACTIVE",
"source": "API_INTEGRATION"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
eventType | string | Yes | Dot-separated event category, e.g. contact.created, login.success, export.completed |
tenantId | string | Yes | Your tenant UUID. Used for multi-tenant data isolation |
eventId | UUID | No | Idempotency key. Auto-generated if omitted |
occurredAt | ISO 8601 | No | When the event happened. Defaults to server receipt time |
actorType | string | No | Who triggered the event: TENANT_STAFF, CONTACT, SYSTEM |
actorId | string | No | UUID of the actor (staff UUID, contact UUID, etc.) |
entityType | string | No | Type of entity affected: CONTACT, ORGANISATION, HOUSEHOLD, NOTE, etc. |
entityId | string | No | UUID of the entity affected |
schemaVersion | integer | No | Version of your event schema for forward-compatibility. Defaults to 1 |
attributes | object | No | Arbitrary string key-value pairs for event-specific metadata |
Example
curl -s -X POST "https://api.altafid.dev.altafid.net/api/analytics/events" \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "x-tenant-uuid: T019A16FF372A70B5A9307B00CE85E4DA" \
-H "x-user-email: advisor@conpat.com" \
-H "x-user-id: TS019C06688AA57248B9EA5325DBDFC48E" \
-H "x-user-type: TENANT_STAFF" \
-H "Content-Type: application/json" \
-d '{
"eventType": "contact.imported",
"tenantId": "T019A16FF372A70B5A9307B00CE85E4DA",
"actorType": "SYSTEM",
"entityType": "CONTACT",
"entityId": "C019B9D0FD0E5772F9E8ED189895D40C4",
"attributes": {
"source": "CRM_MIGRATION",
"batchId": "batch-2026-04-22-001"
}
}'
Response: 202 Accepted with no body. The event has been accepted for processing.
Analytics query
POST/api/analytics/query
Execute an analytical query over ingested events. Supports aggregations (count, sum, avg, min, max), time bucketing (by minute, hour, day, week, or month), field-level filtering, grouping, and result sorting. The query always returns a structured result set with column metadata.
Request body
{
"eventType": "contact.created",
"timeRange": {
"from": "2026-04-01T00:00:00Z",
"to": "2026-04-22T23:59:59Z"
},
"granularity": "DAY",
"filters": [
{ "field": "actorType", "op": "EQ", "value": "TENANT_STAFF" }
],
"groupBy": [
{ "field": "actorId" }
],
"aggregations": [
{ "function": "COUNT", "alias": "total_events" },
{ "function": "COUNT", "field": "entityId", "alias": "unique_contacts" }
],
"sort": [
{ "field": "total_events", "ascending": false }
],
"limit": 100
}
| Field | Type | Required | Description |
|---|---|---|---|
eventType | string | Yes | Filter to a specific event type (e.g. contact.created) |
timeRange | object | Yes | Object with from and to as ISO 8601 timestamps. Defines the query window |
granularity | string | No | Time bucketing resolution. See TimeGranularity values. Defaults to NONE (no bucketing) |
filters | array | No | Array of field-level filter conditions. Each has field, op, and value |
groupBy | array | No | Array of { "field": "fieldName" } objects for result grouping |
aggregations | array | Yes | At least one aggregation required. Each has function, optional field, and alias |
sort | array | No | Array of { "field": "...", "ascending": true/false } for result ordering |
limit | integer | No | Maximum number of rows to return |
Response body
{
"columns": [
{ "name": "time_bucket", "label": "Date", "type": "TIMESTAMP", "role": "TIME" },
{ "name": "actorId", "label": "Staff", "type": "STRING", "role": "DIMENSION" },
{ "name": "total_events", "label": "Total Events", "type": "NUMBER", "role": "METRIC", "primary": true }
],
"rows": [
{ "values": ["2026-04-22T00:00:00Z", "TS019C06688AA57248B9EA5325DBDFC48E", 45] },
{ "values": ["2026-04-21T00:00:00Z", "TS019C06688AA57248B9EA5325DBDFC48E", 32] }
],
"generatedAt": "2026-04-22T15:30:00Z",
"stats": {
"rowCount": 2,
"queryTimeMs": 45
}
}
The columns array describes the shape of each row in rows. Values in each row correspond positionally to the column list. Use columns[i].name as the key and columns[i].type to cast the value correctly.
| Response field | Description |
|---|---|
columns | Column metadata: name, label, type, role, and whether it is the primary metric |
rows | Each row is an object with a values array matching the columns order |
generatedAt | Server timestamp when the query was executed |
stats.rowCount | Total number of rows returned |
stats.queryTimeMs | Server-side query execution time in milliseconds |
Example: daily contact creation by advisor
curl -s -X POST "https://api.altafid.dev.altafid.net/api/analytics/query" \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "x-tenant-uuid: T019A16FF372A70B5A9307B00CE85E4DA" \
-H "x-user-email: advisor@conpat.com" \
-H "x-user-id: TS019C06688AA57248B9EA5325DBDFC48E" \
-H "x-user-type: TENANT_STAFF" \
-H "Content-Type: application/json" \
-d '{
"eventType": "contact.created",
"timeRange": {
"from": "2026-04-01T00:00:00Z",
"to": "2026-04-30T23:59:59Z"
},
"granularity": "DAY",
"groupBy": [{ "field": "actorId" }],
"aggregations": [
{ "function": "COUNT", "alias": "contacts_created" }
],
"sort": [{ "field": "contacts_created", "ascending": false }],
"limit": 50
}'
Enum reference
TimeGranularity
Controls how results are bucketed by time. When set, a time_bucket column is automatically added to the result.
ComparisonOperator (for filters)
Used in the op field of each filter object.
EQ/NEQ— equals / not equals (string or number)GT/GTE— greater than / greater than or equalLT/LTE— less than / less than or equalIN/NOT_IN— value is in / not in a list (pass array asvalue)
AggregationFunction
Used in the function field of each aggregation object. The field is optional for COUNT (counts all rows if omitted).
ColumnType (in responses)
Indicates the data type of a column in the result. Use this to correctly parse values in the rows array.
ColumnRole (in responses)
Indicates the semantic role of a column. Useful for driving charts and visualisation logic.
TIME— the time bucket column (only present whengranularityis notNONE)DIMENSION— a groupBy field (categorical, use for chart axes or row labels)METRIC— an aggregated value (numeric, use for chart values). The primary metric has"primary": trueUNKNOWN— role could not be determined
Errors
| Status | Meaning |
|---|---|
202 Accepted | Event accepted for ingestion (success for POST /events) |
400 Bad Request | Missing required fields (eventType, tenantId, aggregations, or timeRange) or invalid enum value |
401 Unauthorized | Missing or invalid JWT token |
403 Forbidden | Token valid but insufficient permissions for the requested tenant |
500 Internal Server Error | Query execution failed — check field names and operator compatibility |