mobieusKnow mobieusCore API — Event-type reference History #61
Author
Patrick Bass
Submitted
May 28, 2026 6:25pm
Reviewed
May 28, 2026 6:25pm
Summary
Seeded by API feature deploy
# 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`
Every event lands in the per-tenant event log and (if matching) gets fanned out to any subscribed webhook endpoint. The catalog grows over time; this page is the canonical list of what fires today.
+ Fires when a new thread OP is posted OR a reply is added.
## Event envelope
```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" }
+ }
"id": "evt_01j0wnp3a2…", // ULID-style, sortable
"type": "post.created", // public type (this catalog)
"created_at":"2026-05-29T13:42:11.812Z",
"data": { /* per-type shape */ }
}
```
+ - `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.
## Catalog
+ ### `post.edited`
| Type | Fires when |
|-------------------------------|-----------------------------------------------------------------------------|
| `post.created` | A new thread OP or reply is posted. |
| `post.edited` | A post is edited. |
| `post.deleted` | A post is soft-deleted. |
| `user.joined` | A user completes registration. |
| `user.left` | A user schedules account deletion. |
| `listing.created` | A marketplace listing goes live. *(2026-05-29)* |
| `listing.updated` | A marketplace listing is edited. *(2026-05-29)* |
| `listing.sold` | A listing's final unit sells out. *(2026-05-29)* |
| `listing.withdrawn` | A listing is withdrawn by owner or moderator. *(2026-05-29)* |
| `file.uploaded` | A file upload completes quarantine and is recorded. *(2026-05-29)* |
| `report.created` | A user submits a moderation report. *(2026-05-29)* |
| `report.resolved` | A report is resolved (via UI or `POST /api/v1/reports/{id}/resolve`). |
| `moderation.action_taken` | A moderator acts on a report. The `data.action` field tells you which action: `warn`, `suspend`, `ban`, `shadow_ban`, or `dismiss`. *(2026-05-29)* |
| `moderation.user_banned` | Legacy alias for `moderation.action_taken` with `action="ban"`. Will be deprecated. |
| `moderation.user_shadow_banned` | Legacy alias. Will be deprecated. |
| `moderation.user_suspended` | Legacy alias. Will be deprecated. |
| `moderation.user_warned` | Legacy alias. Will be deprecated. |
| `moderation.content_removed` | A moderator removes flagged content. |
| `moderation.content_flagged` | Content is reported for moderation. |
## New event-data shapes (2026-05-29)
### `listing.created`
```json
{
+ "type": "post.edited",
+ "data": {
+ "post_id": 9182,
+ "thread_id": 1124,
+ "editor": { "id": 42, "username": "patrick" }
+ }
"listing_id": 42, "slug": "atari-2600-light-sixer",
"title": "Atari 2600 Light Sixer", "price_cents": 12500,
"category_id": 7,
"seller": { "id": 312, "username": "retroseller" }
}
```
+
+ ### `post.deleted`
+
+ Soft delete (the row stays for moderator review; the public post_count drops).
### `listing.sold`
```json
+ {
+ "type": "post.deleted",
+ "data": {
+ "post_id": 9182,
+ "thread_id": 1124,
+ "by": { "id": 7, "username": "moderator" },
+ "reason": null
+ }
+ }
{ "listing_id": 42, "slug": "atari-2600-light-sixer", "seller_id": 312 }
```
+
+ ### `user.joined`
+
+ Fires when registration is complete and the user has a verified email.
### `file.uploaded`
```json
{
+ "type": "user.joined",
+ "data": {
+ "user_id": 555,
+ "username": "newbie_42",
+ "email_verified": true
+ }
"file_id": 18, "area_slug": "atari-2600-disks",
"title": "Centipede (1981)", "size_bytes": 16384,
"mime": "application/octet-stream",
"uploader": { "id": 312, "username": "retroseller" }
}
```
+
+ ### `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.
### `report.created`
```json
{
+ "type": "user.left",
+ "data": {
+ "user_id": 555,
+ "username": "leaving_user",
+ "reason": "deletion_scheduled"
+ }
"report_id": 91, "reporter": { "id": 7 },
"reported_user_id": 312, "reported_post_id": 4501,
"reason_category": "spam", "source_url": "/thread/foo"
}
```
+
+ ### Moderation events
+
+ All five moderation events share the same `data` shape:
### `moderation.action_taken`
```json
{
+ "type": "moderation.user_banned",
+ "data": {
+ "target": { "kind": "user", "id": 999 },
+ "actor": { "id": 7 },
+ "detail": "Repeated harassment after warning."
+ }
"action": "suspend",
"target_user_id": 312, "report_id": 91,
"duration_hours": 168,
"actor": { "id": 9, "username": "mod_alice" }
}
+ ```
+
+ | 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:
## Filtering
+ - 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.
`GET /api/v1/events?type=post.created&since=2026-05-28T00:00:00Z` returns only matching events newest-first. Pass back the `next_cursor` field to walk history.

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" }
  }
}
  • 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

{
  "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.edited arriving after post.created for the same post_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.