Push your own feature events into Connect's audit log, so an operator debugging a customer sees your application's side of the story next to Connect's.

```ts
await connect.logs.write({
  workspaceId,
  action: "campaign.sent",
  actorUserId,
  metadata: { campaignId, recipients: 1_240 },
});
```

Entries land in Console → Security → Audit, filtered by your application.

**This is an audit trail, not a metrics pipe.** One entry per meaningful, attributable action a human or your system took. Per-request logging belongs in your own observability stack - and `metadata` is read by operators during incidents, so keep credentials, tokens and message bodies out of it.

For counts and charts you want **Analytics** instead.

## Endpoints

### POST `/api/v1/application/logs`

Push a feature event into Connect's audit log

**Scope** `logs:write` &nbsp;·&nbsp; **Rate limit** 120 requests / min per key

Connect has no visibility into your application's own product/feature behavior - it only ever sees the interactions in this reference (sign-ins, RBAC/subscription checks). This endpoint lets you push your own structured events here instead, so they show up in the Console's audit log - filtered to your application from its own page - without needing a separate observability stack.

#### Implementation

**1. Prerequisite** - an API key scoped to `logs:write`.

**2. Send events as they happen:**
```bash
curl -X POST https://connect.cosmoops.com/api/v1/application/logs \
  -H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT" \
  -H "Content-Type: application/json" \
  -d '{"level":"info","message":"campaign sent","metadata":{"campaignId":"..."}}'
```
`level` defaults to `"info"` if omitted. `metadata` is capped at ~16KB serialized. `timestamp` defaults to receipt time - only honored if it parses, and never trusted to reorder ingestion.

**3. Attribute it to a user** by passing that user's `sessionToken` - the same opaque token `/sso/token` handed you at sign-in, and the one `/sso/userinfo` takes:
```bash
  -d '{"level":"info","message":"campaign sent","sessionToken":"THEIR_SESSION_TOKEN"}'
```
Both scopes on the stored entry come from something verified, never from your body: `applicationId` from the API key, and `userId` from this token - which is checked to be unrevoked, unexpired, and **minted for your application** before anything is attributed. There is no `userId` field to send; a caller-typed actor would make the log stream unusable as a record of who did what.

The response's `attributed` says whether the token resolved. A token that has expired (~15 days) or was revoked yields `attributed: false` and the event is **still stored**, just without a user - losing telemetry because an attribution failed would be the worse outcome of the two.

**4. That's it** - entirely optional, fire-and-forget, and only ever visible in this application's own Logs section, never mixed with another application's or with Connect's own audit trail.

#### Errors
`validation_error` (400), `forbidden` (403 - key missing `logs:write`), `rate_limited` (429 - meaningfully higher than the read endpoints since this is meant to carry real event volume, not an occasional check).

**Request body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `level` | `debug` \| `info` \| `warn` \| `error` | no |  |
| `message` | `string` | yes |  |
| `metadata` | `object` | no |  |
| `timestamp` | `any` | no |  |
| `sessionToken` | `string` | no |  |

**Responses**

| Status | Meaning |
| --- | --- |
| `201` | Logged |
| `400` | validation_error |
| `403` | forbidden - key missing logs:write |
| `429` | rate_limited |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `success` | `boolean` | yes |  |
| `attributed` | `boolean` | yes |  |