Event-type reference
Every event in /api/v1/events (and every webhook payload in Phase 2) uses the same envelope:
{
"id": "evt_01JG5K8HW2X4A8Q3M1T6KQ7BWP",
"type": "post.created",
"created_at": "2026-05-28T12:34:56.123456Z",
"data": { ...resource snapshot... }
}
id— ULID-style, sortable, unique. Use it for deduplication.type— dotted, stable, versioned. We won't rename existing types without a major-version bump.created_at— UTC, microsecond precision, ISO-8601.data— the resource snapshot relevant to this event. Schema varies by type — see below.
Event types
post.created
Fires when a new thread OP is posted OR a reply is added.
{
"id": "evt_…",
"type": "post.created",
"created_at": "2026-05-28T12:34:56.123456Z",
"data": {
"post_id": 9182,
"thread_id": 1124,
"forum_slug": "general",
"title": "Welcome to the community",
"parent_post_id": null,
"author": { "id": 42, "username": "patrick" }
}
}
titleis present only on the thread OP, not on replies.parent_post_idis non-null only when the reply is nested under another reply.
post.edited
{
"type": "post.edited",
"data": {
"post_id": 9182,
"thread_id": 1124,
"editor": { "id": 42, "username": "patrick" }
}
}
post.deleted
Soft delete (the row stays for moderator review; the public post_count drops).
{
"type": "post.deleted",
"data": {
"post_id": 9182,
"thread_id": 1124,
"by": { "id": 7, "username": "moderator" },
"reason": null
}
}
user.joined
Fires when registration is complete and the user has a verified email.
{
"type": "user.joined",
"data": {
"user_id": 555,
"username": "newbie_42",
"email_verified": true
}
}
user.left
Fires when a user schedules their account for deletion. The account stays in a 30-day grace window before the row is purged.
{
"type": "user.left",
"data": {
"user_id": 555,
"username": "leaving_user",
"reason": "deletion_scheduled"
}
}
Moderation events
All five moderation events share the same data shape:
{
"type": "moderation.user_banned",
"data": {
"target": { "kind": "user", "id": 999 },
"actor": { "id": 7 },
"detail": "Repeated harassment after warning."
}
}
| Type | Fires when |
|---|---|
moderation.user_warned |
A moderator issues a warning. |
moderation.user_suspended |
A moderator suspends an account (timed). |
moderation.user_banned |
A moderator bans an account. |
moderation.user_shadow_banned |
A moderator shadow-bans (content invisible to others, user not notified). |
moderation.content_removed |
A moderator removes a flagged file or post. |
moderation.content_flagged |
A community member reports content. |
detail is truncated to 200 characters and may be null.
Filtering and pagination
# All events
GET /api/v1/events
# Just bans, last 24h
GET /api/v1/events?type=moderation.user_banned&since=2026-05-27T00:00:00Z
# Page through 100 at a time
GET /api/v1/events?limit=100
# Response includes: "next_cursor": "MjAyNi0w..."
GET /api/v1/events?limit=100&cursor=MjAyNi0w...
The cursor is opaque and stable — pass it back exactly as received. Cursor pagination is safe against concurrent inserts: pages never overlap or skip rows.
Ordering and delivery (looking ahead to webhooks)
Events are stored in arrival order and the API returns them newest-first. When webhooks ship in Phase 2:
- Delivery is best-effort, at-least-once. Receivers must be idempotent on
event.id. - Order is best-effort. Don't depend on
post.editedarriving afterpost.createdfor the samepost_id. - Retries are exponential: roughly 0s → 1m → 5m → 30m → 2h → 6h → 24h, then dead-letter.
# Event-type reference
Every event in `/api/v1/events` (and every webhook payload in Phase 2) uses the same envelope:
```json
{
"id": "evt_01JG5K8HW2X4A8Q3M1T6KQ7BWP",
"type": "post.created",
"created_at": "2026-05-28T12:34:56.123456Z",
"data": { ...resource snapshot... }
}
```
- **`id`** — ULID-style, sortable, unique. Use it for deduplication.
- **`type`** — dotted, stable, versioned. We won't rename existing types without a major-version bump.
- **`created_at`** — UTC, microsecond precision, ISO-8601.
- **`data`** — the resource snapshot relevant to this event. Schema varies by type — see below.
## Event types
### `post.created`
Fires when a new thread OP is posted OR a reply is added.
```json
{
"id": "evt_…",
"type": "post.created",
"created_at": "2026-05-28T12:34:56.123456Z",
"data": {
"post_id": 9182,
"thread_id": 1124,
"forum_slug": "general",
"title": "Welcome to the community",
"parent_post_id": null,
"author": { "id": 42, "username": "patrick" }
}
}
```
- `title` is present only on the thread OP, not on replies.
- `parent_post_id` is non-null only when the reply is nested under another reply.
### `post.edited`
```json
{
"type": "post.edited",
"data": {
"post_id": 9182,
"thread_id": 1124,
"editor": { "id": 42, "username": "patrick" }
}
}
```
### `post.deleted`
Soft delete (the row stays for moderator review; the public post_count drops).
```json
{
"type": "post.deleted",
"data": {
"post_id": 9182,
"thread_id": 1124,
"by": { "id": 7, "username": "moderator" },
"reason": null
}
}
```
### `user.joined`
Fires when registration is complete and the user has a verified email.
```json
{
"type": "user.joined",
"data": {
"user_id": 555,
"username": "newbie_42",
"email_verified": true
}
}
```
### `user.left`
Fires when a user schedules their account for deletion. The account stays in a 30-day grace window before the row is purged.
```json
{
"type": "user.left",
"data": {
"user_id": 555,
"username": "leaving_user",
"reason": "deletion_scheduled"
}
}
```
### Moderation events
All five moderation events share the same `data` shape:
```json
{
"type": "moderation.user_banned",
"data": {
"target": { "kind": "user", "id": 999 },
"actor": { "id": 7 },
"detail": "Repeated harassment after warning."
}
}
```
| Type | Fires when |
|---|---|
| `moderation.user_warned` | A moderator issues a warning. |
| `moderation.user_suspended` | A moderator suspends an account (timed). |
| `moderation.user_banned` | A moderator bans an account. |
| `moderation.user_shadow_banned` | A moderator shadow-bans (content invisible to others, user not notified). |
| `moderation.content_removed` | A moderator removes a flagged file or post. |
| `moderation.content_flagged` | A community member reports content. |
`detail` is truncated to 200 characters and may be null.
## Filtering and pagination
```bash
# All events
GET /api/v1/events
# Just bans, last 24h
GET /api/v1/events?type=moderation.user_banned&since=2026-05-27T00:00:00Z
# Page through 100 at a time
GET /api/v1/events?limit=100
# Response includes: "next_cursor": "MjAyNi0w..."
GET /api/v1/events?limit=100&cursor=MjAyNi0w...
```
The cursor is **opaque and stable** — pass it back exactly as received. Cursor pagination is safe against concurrent inserts: pages never overlap or skip rows.
## Ordering and delivery (looking ahead to webhooks)
Events are stored in arrival order and the API returns them newest-first. When webhooks ship in Phase 2:
- Delivery is **best-effort, at-least-once.** Receivers must be **idempotent on `event.id`.**
- Order is best-effort. Don't depend on `post.edited` arriving after `post.created` for the same `post_id`.
- Retries are exponential: roughly 0s → 1m → 5m → 30m → 2h → 6h → 24h, then dead-letter.