Analytics

External integration

The 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.

Base path: All analytics endpoints are under /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"
  }
}
FieldTypeRequiredDescription
eventTypestringYesDot-separated event category, e.g. contact.created, login.success, export.completed
tenantIdstringYesYour tenant UUID. Used for multi-tenant data isolation
eventIdUUIDNoIdempotency key. Auto-generated if omitted
occurredAtISO 8601NoWhen the event happened. Defaults to server receipt time
actorTypestringNoWho triggered the event: TENANT_STAFF, CONTACT, SYSTEM
actorIdstringNoUUID of the actor (staff UUID, contact UUID, etc.)
entityTypestringNoType of entity affected: CONTACT, ORGANISATION, HOUSEHOLD, NOTE, etc.
entityIdstringNoUUID of the entity affected
schemaVersionintegerNoVersion of your event schema for forward-compatibility. Defaults to 1
attributesobjectNoArbitrary 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
}
FieldTypeRequiredDescription
eventTypestringYesFilter to a specific event type (e.g. contact.created)
timeRangeobjectYesObject with from and to as ISO 8601 timestamps. Defines the query window
granularitystringNoTime bucketing resolution. See TimeGranularity values. Defaults to NONE (no bucketing)
filtersarrayNoArray of field-level filter conditions. Each has field, op, and value
groupByarrayNoArray of { "field": "fieldName" } objects for result grouping
aggregationsarrayYesAt least one aggregation required. Each has function, optional field, and alias
sortarrayNoArray of { "field": "...", "ascending": true/false } for result ordering
limitintegerNoMaximum 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 fieldDescription
columnsColumn metadata: name, label, type, role, and whether it is the primary metric
rowsEach row is an object with a values array matching the columns order
generatedAtServer timestamp when the query was executed
stats.rowCountTotal number of rows returned
stats.queryTimeMsServer-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.

NONE MINUTE HOUR DAY WEEK MONTH

ComparisonOperator (for filters)

Used in the op field of each filter object.

EQ NEQ GT GTE LT LTE IN NOT_IN
  • EQ / NEQ — equals / not equals (string or number)
  • GT / GTE — greater than / greater than or equal
  • LT / LTE — less than / less than or equal
  • IN / NOT_IN — value is in / not in a list (pass array as value)

AggregationFunction

Used in the function field of each aggregation object. The field is optional for COUNT (counts all rows if omitted).

COUNT SUM AVG MIN MAX

ColumnType (in responses)

Indicates the data type of a column in the result. Use this to correctly parse values in the rows array.

STRING NUMBER BOOLEAN TIMESTAMP

ColumnRole (in responses)

Indicates the semantic role of a column. Useful for driving charts and visualisation logic.

TIME DIMENSION METRIC UNKNOWN
  • TIME — the time bucket column (only present when granularity is not NONE)
  • 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": true
  • UNKNOWN — role could not be determined

Errors

StatusMeaning
202 AcceptedEvent accepted for ingestion (success for POST /events)
400 Bad RequestMissing required fields (eventType, tenantId, aggregations, or timeRange) or invalid enum value
401 UnauthorizedMissing or invalid JWT token
403 ForbiddenToken valid but insufficient permissions for the requested tenant
500 Internal Server ErrorQuery execution failed — check field names and operator compatibility