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.

```ts
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` &nbsp;·&nbsp; **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:**
```bash
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` &nbsp;·&nbsp; **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:**
```bash
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 |  |