# System tokens — mint, rotate, retention, revoke

A **system token** is the credential your code uses to ingest into ActivityLog. Every POST carries `Authorization: Bearer al_…` against the receiver.

There are two token shapes:

| Prefix | Purpose | Used at |
|---|---|---|
| `al_…` | Native ingest + OTLP + Langfuse | `POST /messages`, `POST /otlp/v1/logs`, `POST /otlp/v1/traces`, `POST /api/public/ingestion` |
| `alw_…` | Webhook receiver | `POST /webhooks/{alw_…}` |

This page covers `al_…`. For webhook tokens see [`53-ingest-webhooks.md`](./53-ingest-webhooks.md).

## Mint a token

1. Sidebar → **Systems** → click the system (or **New System** if you don't have one).
2. On the system detail page click **New Token**.
3. Pick a **retention** (see below).
4. Click **Generate**.
5. The token is shown **once**. Copy it now to your secret store.

![Token mint dialog with retention dropdown](screenshots/portal-systems-token-mint.png)

> The portal will never show you the token value again. We store only a one-way hash — there is no recovery path. If you lose it, revoke and mint a new one.

## Retention — per token, not per tenant

Each token has its own retention window. This is the platform's key differentiator: a single system can have **two tokens**, one with 5-year retention (for compliance-shaped events) and one with 90-day retention (for high-volume analytics).

Allowed values:

| Days | Use case |
|---|---|
| 7 | Free-tier default; ephemeral testing |
| 30 | Short-lived dev environments |
| 90 | Pro-tier default; normal app activity |
| 180 | Half-year audit window |
| 365 | 1y — generic ingest, no specific compliance need |
| 730 | 2y |
| 1095 | 3y |
| 1825 | 5y — typical security-audit window; recommended for M365 sources |
| -1 | Never expire (Enterprise only) |

The portal dropdown shows only values your tier allows. Free is capped at 7. Pro is capped at 90 unless you've negotiated an override. Enterprise can pick any value including `-1`.

### Changing retention after mint

**Systems → {system} → {token} → Edit retention.** The new value applies to **future** messages — already-stored messages keep the retention they were ingested under.

![Token retention edit panel](screenshots/portal-systems-token-retention-edit.png)

## Rotation

There's no "rotate" button — rotation is **mint-new + revoke-old**:

1. Mint a new token under the same system.
2. Deploy the new token to your app(s).
3. Verify ingest is flowing under the new token (Messages view shows the system + token id on each message).
4. **Systems → {system} → Tokens → {old token} → Revoke**.

The revoked token rejects all subsequent requests with **401 Unauthorized**.

![Tokens table with Revoke action](screenshots/portal-systems-tokens-table.png)

> Tokens have no expiry by default. Rotate every 90 days as a habit even if there's no leak suspected; rotation cost is one redeploy + one revoke click.

## Revocation

A revoked token cannot ingest. The data it ingested is **not** affected — only future requests fail.

To revoke: **Systems → {system} → Tokens → Revoke**. There's no undo. If you revoke by accident, mint a new one.

## What a token can and can't do

A system token is **write-only and scoped to one system in one tenant**. With a token you can:

- POST to `/messages` (single + batch) for the bound system
- POST to `/otlp/v1/logs` and `/otlp/v1/traces` for the bound system
- POST to `/api/public/ingestion` (Langfuse) for the bound system
- GET `/systems/me` to confirm which system the token belongs to

You **cannot**:

- Read any messages back
- Touch any other system
- Touch any other tenant's data
- Mint, rotate, or revoke other tokens (use the portal or a user JWT)

If a token is leaked, the blast radius is one system's ingest — not the whole tenant.

## Best practices

- **One token per source environment.** Production app gets its own token; staging gets its own; CI gets its own. When something goes wrong you can revoke just that environment.
- **Long-retention token for audit-shaped events, short for analytics.** Most M365 integrations use 5-year tokens. App logs typically use 90-day.
- **Never embed in client-side code.** Tokens are bearer credentials — anyone who sees the JS source can ingest as you. For browser/mobile clients, proxy through your backend.
- **Watch the headers.** Successful ingest returns `X-RateLimit-Remaining` — log it. When you see it falling below 10% of capacity, you're about to hit the burst cap.
- **Free-tier sampling.** Above the burst cap on Free, the API returns `X-AL-Sampled: true` and drops 1-in-N requests. If you see that header in logs, you're outgrowing Free.

## Tier caps recap

| Capability | Free | Pro | Enterprise |
|---|---|---|---|
| Tokens per system | 2 | 50 | unlimited |
| Retention range | 1–7 days | 7–90 days | per contract |
| Steady ingest rate | 100 msg/s | 5,000 msg/s | per contract |
| Burst (10 s) | 1,000 msg/s | 50,000 msg/s | per contract |
| Sampling above burst? | yes | no | no |

Numbers from [`../Pricing.md`](../Pricing.md).

## What's next

| Goal | Doc |
|---|---|
| POST your first message | [`00-quickstart.md`](./00-quickstart.md) |
| Native ingest deep-dive (batch, dedup, duration, metadata) | [`50-ingest-native.md`](./50-ingest-native.md) |
| OpenTelemetry collector setup | [`51-ingest-otlp.md`](./51-ingest-otlp.md) |
| Webhook token (for Event Grid / ADO) | [`53-ingest-webhooks.md`](./53-ingest-webhooks.md) |

## Troubleshooting

**`401 Unauthorized` — "invalid token".**
The token is wrong, revoked, or scoped to a different environment than the host you're hitting. Check the system detail page in the portal to confirm the token still shows as **Active**.

**`429 Too Many Requests` with `Retry-After: N`.**
You've hit the burst cap. On Free, you'll also see `X-AL-Sampled: true` on subsequent requests — some are dropped silently. Either upgrade to Pro, or batch your POSTs.

**`413 Payload Too Large`.**
Your message body is over the tier inline cap (16 KB on Free, 256 KB on Pro). Either shrink the body (most over-the-cap payloads embed something that should be a separate Mode-2 ingest), or upgrade.

**My token works on staging but fails on production.**
Staging and production are separate environments with separate databases. Tokens minted on `activitylog-api.betawebserver.com` will not work on `api.activitylog.com`.
