---
title: "Events"
description: "Follow your apps with durable events, a live stream, and streaming drafts instead of polling."
canonical_url: "https://hoplite.sh/docs/platform/events"
markdown_url: "https://hoplite.sh/docs/platform/events.md"
---

# Events
URL: /docs/platform/events
LLM index: /llms.txt
Description: Follow your apps with durable events, a live stream, and streaming drafts instead of polling.

# Events

Every change inside an app publishes a durable event: the app itself being created, updated, or deleted, source imports, applies, builds, head moves, and the threads, runs, messages, and tool calls of its editing threads. Events identify the resource and its public status. Read the resource through the API for its details.

The Platform API has two feeds. Both require `thread:read` and the organization's Platform API access.

| Feed | Endpoints | Contains |
| --- | --- | --- |
| One app | `GET /api/platform/v1/apps/{appId}/events`, `/events/head`, `/events/stream` | That app's events |
| All apps | `GET /api/platform/v1/events`, `/events/head`, `/events/stream` | Every app the credential can read |

A credential restricted to specific apps reads only those apps: other apps' feeds answer `403`, and the all-apps feed leaves their events out. The Direct `/api/events` feed and Direct webhooks never include a Platform app's events.

## Load a consistent view

1. Capture a cursor from the feed's `/events/head`, with the filters you will use.
2. Read the app, thread, or run snapshots your view needs.
3. Replay `/events` from that cursor with the same filters, then keep polling or connect to `/events/stream`.

Capturing the head first means an event committed while you read snapshots is replayed rather than missed.

```sh
curl "$HOPLITE_API_URL/api/platform/v1/apps/$APP_ID/events/head" \
  -H "Authorization: Bearer $HOPLITE_SERVICE_KEY"

curl "$HOPLITE_API_URL/api/platform/v1/apps/$APP_ID/events?cursor=$CURSOR" \
  -H "Authorization: Bearer $HOPLITE_SERVICE_KEY"
```

```json
{
  "ok": true,
  "events": [
    {
      "id": "…",
      "type": "run.completed",
      "sequence": "42",
      "orgId": "org_…",
      "appId": "app_…",
      "projectId": "app_…",
      "threadId": "thr_…",
      "runId": "run_…",
      "data": { "id": "run_…", "status": "completed" },
      "occurredAt": "2026-09-26T10:00:00.000Z",
      "publishedAt": "2026-09-26T10:00:01.000Z",
      "cursor": "…"
    }
  ],
  "nextCursor": "…",
  "hasMore": false
}
```

Every event names its app in `appId`; `projectId` carries the same value. Narrow a feed with `threadId`, `runId`, and comma-separated `types`. A `threadId` must belong to the feed, otherwise the request answers `404 thread_not_found`. `limit` defaults to 100 and allows up to 500.

## App events

| Type | Published when | `data` fields |
| --- | --- | --- |
| `platform.app.created` | An app is created | `id` (the app), `externalId`, `createdAt`, `updatedAt` |
| `platform.app.updated` | An app is updated with `PATCH` | `id`, `externalId`, `createdAt`, `updatedAt` |
| `platform.app.deleted` | An app is deleted | `id`, `externalId`, `createdAt`, `updatedAt` |
| `platform.source.imported` | A source version is stored | `id`, `sha256`, `byteSize`, `fileCount` |
| `platform.apply.<status>` | A draft apply is queued, applied, conflicted, or failed | `id`, `status`, `sourceId` (the result) |
| `platform.build.<status>` | A build is queued, building, ready, or failed | `id`, `status`, `sourceId`, `artifactSha256` when ready |
| `platform.head.updated` | The app head moves | `id` (the app), `sourceId`, `revision` |

`externalId` is `null` when the app has none, including after you clear it. The app's `name` and `metadata` are never published, because they hold your own content; read the app with `GET /api/platform/v1/apps/{appId}` when you need them. Threads, runs, messages, and tool calls publish the same types and fields as the [Direct event types](/docs/factory/events#event-types).

After an app is deleted, its own feed answers `404 app_not_found`. A stream already open on that app still receives `platform.app.deleted`. The all-apps feed keeps `platform.app.deleted`, like every event, for seven days.

## Stream and reconnect

`/events/stream` is a server-sent event stream. Each frame's event name is the event type and its data is the event JSON. Persist the frame's `id` (the event `cursor`) after processing it and send it as `Last-Event-ID` when you reconnect with identical filters. Deduplicate by the JSON event `id`. Heartbeats arrive every 15 seconds; the stream rechecks authorization, including the organization's Platform API access, and closes when access ends.

With `threadId`, the stream also sends `draft` frames carrying the assistant's in-progress reply, and `draft.removed` when a persisted message supersedes one. Replace, rather than append, the draft for a message when a newer revision arrives. Drafts have no cursor or replay guarantee.

Cursors are bound to the feed and its filters: a cursor from one app's feed is rejected by the all-apps feed and by the Direct API with `400 invalid_cursor`. An all-apps cursor is also bound to the credential's apps. When a restricted credential gains or loses an app, its all-apps cursors answer `410 cursor_scope_changed`, and an open all-apps stream sends an `error` frame with `cursor_scope_changed` and closes at its next authorization check; otherwise a newly added app's earlier events would be skipped, or a removed app's would keep arriving. Events are retained for seven days. On `410 cursor_expired` or `410 cursor_scope_changed`, capture a new head, reload your snapshots, and replay from the new cursor.

## Webhooks

Webhooks for Platform apps are not available yet. Direct webhooks never deliver a Platform app's events, including deliveries queued before this rule, which are recorded as failed with `platform_app_event`. A Direct webhook cannot be scoped to an app. Consume the event stream, or poll `/events` from your backend, and relay updates to your users through your own application.

## Sitemap

See the full [sitemap](/docs/sitemap.md) for all pages.
Well-known sitemap: [/docs/.well-known/sitemap.md](/docs/.well-known/sitemap.md).
