Analytics
Emit your own product events, and read back aggregated usage, API volume and revenue for this application.
Your own product events, and the aggregates built from them.
POST /analytics/events emits events; GET /analytics reads back usage, API volume and revenue for your application.
await connect.analytics.emit([
{ name: "campaign_sent", occurredAt: new Date().toISOString(), value: 1 },
]);
const { series } = await connect.analytics.series({
metrics: ["usage.events", "api.requests"],
from,
to,
});Send events in batches - one request per user action is a request per user action.
Events are bucketed by UTC day. An occurredAt far in the past or the future is clamped rather than rejected, so a client with a wrong clock cannot rewrite history or poison tomorrow. Backfill has a bounded window; anything older is clamped to it.
Distinct from Logs: an audit entry is one attributable action a person can be asked about, an analytics event is a number in an aggregate.
Endpoints
POST /api/v1/application/events
Emit your own product events
Scope analytics:write · Rate limit 120 requests / min per key
Pushes your platform's own product events into Connect's analytics pipeline, so your usage shows up alongside the numbers Connect already collects. The sibling of logs:write: that one is a searchable log line, this one is a countable event.
Implementation
1. Prerequisite - an API key scoped to analytics:write.
2. Send up to 50 events per request:
curl -X POST "https://connect.cosmoops.com/api/v1/application/events" \
-H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT" \
-H "Content-Type: application/json" \
-d '{"events":[{"name":"checkout.completed","value":4999,"unit":"paise","sessionToken":"..."}]}'3. Name your events namespace.action, lowercase. Prefixes Connect emits itself (api., billing., addon., subscription., connect.) are rejected, so your events can never be mistaken for - or inflate - Connect's own metered numbers.
4. Pass sessionToken to attribute an event to the signed-in user and their workspace. Without it the event still lands, just unattributed: attributed in the response tells you how many were tied to a user. An expired or wrong-application token is not an error either, for the same reason - losing the event would be worse than losing its attribution.
value is a whole number in the smallest unit of unit (paise, bytes, milliseconds). Fractional measures belong in props.
occurredAt is optional and clamped server-side to within 48 hours - enough to replay a queue after an outage, not enough to rewrite a period that has already been reported.
Errors
forbidden (403 - key missing analytics:write), validation_error (422 - reserved prefix, malformed name, or oversized props), rate_limited (429).
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
events | array | yes |
Responses
| Status | Meaning |
|---|---|
200 | Events accepted |
403 | forbidden - key missing analytics:write |
422 | validation_error |
429 | rate_limited |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
accepted | integer | yes | |
attributed | integer | yes |
GET /api/v1/application/analytics
Read this application's aggregated analytics
Scope analytics:read · Rate limit 60 requests / min per key
Daily numbers for your application - usage volume, API call counts, active users, and subscription/revenue totals - over a date range of up to 90 days.
Implementation
1. Prerequisite - an API key scoped to analytics:read.
2. Send the request:
curl "https://connect.cosmoops.com/api/v1/application/analytics?metrics=usage.events,api.requests&from=2026-08-01&to=2026-08-25" \
-H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT"3. Read series - one entry per requested metric, each with a points array of { day, value }.
A value of null is not zero. It means that day is not measurable for that metric: usage.active_users is a distinct count that only exists after the nightly rollup, so today's is always null, and future days are null for the obvious reason. total sums only the measurable days and partial is true whenever any day was skipped - render that caveat rather than a confident total.
The scope is always your application, taken from the presenting key. There is no parameter for it, by design.
Errors
forbidden (403 - key missing analytics:read), validation_error (422 - unknown metric, inverted range, or a range of 90 days or more), rate_limited (429).
Responses
| Status | Meaning |
|---|---|
200 | Daily metric series |
403 | forbidden - key missing analytics:read |
422 | validation_error |
429 | rate_limited |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
from | string | yes | |
to | string | yes | |
series | array | yes |
GET /api/v1/application/analytics/series
Read your declared metrics as a series
Scope analytics:read · Rate limit 60 requests / min per key
Series for the metrics your application declared in its analytics schema (Console > Applications > Analytics), bucketed by day, week, month or the whole range, in the workspace's own time zone. Up to 400 days per request.
Implementation
1. Prerequisite - an API key scoped to analytics:read, and the metrics declared in your application's analytics schema. A metric reads one event you emit through POST /events (or events on POST /data-commit) with one aggregation: count, sum of value, distinct users, or gauge (the last level pushed).
2. Send the request:
curl "https://connect.cosmoops.com/api/v1/application/analytics/series?metrics=deals.won,deals.won_value&from=2026-10-01&to=2026-10-31&granularity=week&workspaceId=WORKSPACE_ID" \
-H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT"metrics- comma-separated declared metric keys.from,to- inclusive local days,YYYY-MM-DD, in the workspace's time zone (timeZonein the response says which).granularity-day(default),week(buckets start Monday),month(the 1st) ortotal.groupBy- a declared dimension: one series per value, highest volume first. Values beyond the dimension's cardinality fold into_other.filter-dimension:value, restricting to one value of one declared dimension.workspaceId- optional. When present it must be a workspace subscribed to your application, as on every workspace-scoped endpoint. Omit it for the application-wide total across every workspace - a server-side number; never forward it to a browser.
3. Read series - one entry per metric (per dimension value when grouped), each with points of { bucket, value } and a total combined by the metric's own rule: sums add, a gauge is the latest level, distinct users are merged rather than added.
A value of null is not zero. Today comes from live counters; earlier days come from the nightly rollup, and a day it has not reached is null. A distinct metric is null for today. partial is true whenever any day in range was not measurable - render that caveat rather than a confident total.
Errors
forbidden (403 - key missing analytics:read, or a workspaceId not subscribed to your application), validation_error (422 - undeclared metric or dimension, inverted or over-long range, malformed query), rate_limited (429).
Parameters
| Name | In | Required | Notes |
|---|---|---|---|
metrics | query | yes | Comma-separated declared metric keys |
from | query | yes | Inclusive local day, YYYY-MM-DD |
to | query | yes | Inclusive local day, YYYY-MM-DD |
granularity | query | no | |
groupBy | query | no | A declared dimension key |
filter | query | no | dimension:value |
workspaceId | query | no | Omit for the application-wide total |
Responses
| Status | Meaning |
|---|---|
200 | Metric series |
403 | forbidden |
422 | validation_error |
429 | rate_limited |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
from | string | yes | |
to | string | yes | |
granularity | day | week | month | total | yes | |
timeZone | string | yes | The zone the local days are in - the workspace's, or the default for an application-wide read |
series | array | yes |
GET /api/v1/application/analytics/schema
Read your application's analytics schema
Scope analytics:read · Rate limit 60 requests / min per key
The metrics and dimensions your application declared - what GET /analytics/series will accept. Lets a dashboard render its metric picker from the declaration instead of hardcoding it.
Implementation
curl "https://connect.cosmoops.com/api/v1/application/analytics/schema" \
-H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT"The schema is edited in Console (or by your seed script), never by an API key. An application that declared nothing answers { "metrics": [], "dimensions": [] }.
Errors
forbidden (403 - key missing analytics:read), rate_limited (429).
Responses
| Status | Meaning |
|---|---|
200 | The declared schema |
403 | forbidden |
429 | rate_limited |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
metrics | array | yes | |
dimensions | array | yes |