# Change envelope v1

Every change to an event (its date moved, its status changed, it was
listed, a party disputed a fact, CastHQ's confidence in it moved) is one
**change envelope**: a [CloudEvents 1.0](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md)
structured-mode event. The change feed (`GET /v1/changes?format=cloudevents`)
and every subscription firing carry it. Schema:
[/v1/change-envelope.json](/v1/change-envelope.json).

```json
{
  "specversion": "1.0",
  "id": "chg_48213",
  "source": "https://casthq.co",
  "type": "co.casthq.event.schedule.moved_later",
  "subject": "urn:casthq:event:01a022a6-a46a-71b2-80ee-62afe5707562",
  "time": "2026-10-30T14:02:11.000Z",
  "casthqversion": "2026-08-21",
  "datacontenttype": "application/json",
  "data": {
    "cursor": "chg_48213",
    "urn": "urn:casthq:change:chg_48213",
    "event": { "id": "01a0…", "urn": "urn:casthq:event:01a0…", "url": "https://casthq.co/apple/wwdc-2027", "title": "WWDC 2027", "status": "scheduled", "state": "scheduled" },
    "kind": "schedule",
    "semantic": "moved_later",
    "old": { "start": { "precision": "day", "not_before": "…", "not_after": "…" } },
    "new": { "start": { "precision": "day", "not_before": "…", "not_after": "…" } },
    "delta": { "start_seconds": 604800, "end_seconds": null },
    "provenance": { "origin": "producer", "claim": "urn:casthq:claim:01a0…", "source": null, "source_url": "https://…", "evidence_sha256": "9f2c…", "via": "api_key" },
    "cause": null,
    "confidence": null,
    "before": { "status": "scheduled", "phase": "upcoming", "state": "date_confirmed", "listed": true, "precision": "day", "facts": { "schedule": "confirmed" }, "fact_sources": { "schedule": 2, "status": 0, "location": 0 } },
    "after":  { "status": "scheduled", "phase": "upcoming", "state": "date_confirmed", "listed": true, "precision": "day", "facts": { "schedule": "confirmed" }, "fact_sources": { "schedule": 2, "status": 0, "location": 0 } }
  },
  "proof": "eyJhbGciOiJFZERTQSIsImtpZCI6Ii4uLiJ9..Qm9n…"
}
```

## Attributes

| Attribute | Meaning |
|---|---|
| `id` | The change id, also the feed cursor. Deduplicate on (`source`, `id`) |
| `source` | The deployment's web origin, `https://casthq.co` in production |
| `type` | `co.casthq.event.<kind>[.<semantic or new value>]`, see below. Prefix-match it: `co.casthq.event.schedule.` is every date move |
| `subject` | The event's URN |
| `time` | When the change was recorded |
| `casthqversion` | The API version the payload's shape follows |
| `proof` | CastHQ's Ed25519 signature: a detached JWS over the envelope's RFC 8785 canonical JSON without `proof`. Keys: `/.well-known/casthq-keys.json` |
| `data.event` | The event **as it is now**. For the state at the change, read `before` / `after`. On an event variant also `variant` (its value, `{"region": "euw"}`, fixed for life) and `variant_of` (its parent); null otherwise. On an occurrence of a series also `series`: `{ id, occurrence_key, detached, off_rule }` (its series, its key, fixed for life, the facts it holds for itself, and whether the rule still generates it); null otherwise |
| `data.kind`, `data.semantic` | What changed. `semantic` classifies schedule moves and dispute changes |
| `data.old`, `data.new` | The values either side |
| `data.delta` | Schedule changes: how far each side moved, in seconds (positive = later). Null otherwise |
| `data.provenance` | The winning claim: its `origin` (`producer`, `observed`, `admin`), claim URN, `source`, `source_url`, `evidence_sha256` and `via`. Never a credential |
| `data.cause` | The `chg_` id of the change that caused this one, or null |
| `data.confidence` | A `confidence` change only: `{fact, before, after, reasons}` (see [Confidence](/confidence)). The same level either side means a claim moved only the source count |
| `data.approval` | On an approval's changes only (absent otherwise): `{decision, decided_by_name, note}`, the decision now (`approved`, `rejected`, or null while it waits). Only the approval's namespace's readers receive it |
| `data.before`, `data.after` | The event's derived state on either side of the change, **as it was then**: `status`, `phase`, `state`, `listed`, `precision`, a confidence level per fact (`facts`), per fact how many independent sources agree with the shown value (`fact_sources`, a count, not a confidence; absent on older changes), ticket `availability` (null when no source has said; absent on older changes), and `lineup_urns`, the topics the line-up names (empty when none; absent on older changes). `before` is null for `created`. Conditions are judged against these |

## Types

| Kind | Types |
|---|---|
| `schedule` | `.schedule.scheduled`, `.moved_earlier`, `.moved_later`, `.narrowed`, `.widened`, `.undated` |
| `status` | `.status.<new status>`: `announced`, `scheduled`, `live`, `completed`, `cancelled`, `postponed`, `delayed` |
| `published` | `.published.public`, `.unlisted`, `.private`; `.published.ready`, `.unready` |
| `title` | `.title`, or `.title.slug` when the slug was renamed |
| `dispute` | `.dispute.opened`, `.dispute.resolved` |
| `phase` | `.phase.started`, `.ended`, `.upcoming`: the clock passing the event's start or end |
| `confidence` | `.confidence.<level after>` |
| `availability` | `.availability.<new value>`: `not_on_sale`, `on_sale`, `few_left`, `sold_out`, `waitlist`, `unavailable`. `old` / `new` are `{availability, on_sale_at?, on_sale_precision?, url?}` |
| `lineup` | `.lineup`: who takes part moved. `old` / `new` are `{lineup: [{name, role?, urn?}]}` (`old` null for the first) |
| `recurrence` | `.recurrence`: a series' rule moved; `old` and `new` are `{ recurrence: <rule> \| null }` (null: a one-off) |
| `variant` | `.variant`, on a variant parent when it gains a variant: `semantic` `added`, `new` is `{ action, variant, value, key }` with `action` `added` or `adopted` |
| `created`, `location`, `links`, `merged` | `.<kind>` |

Two kinds are reserved and not written yet: `dependency` (an event's date
derived from another's) and `expectation`. Their types will be the bare
`.<kind>` until they are.

The set is open: new kinds appear within a version. Ignore types you do not
know.

## Lifecycle state

`state` is one value for where an event is. The rules are tried in order;
the first that holds wins:

| # | State | Rule |
|---|---|---|
| 1 | `cancelled` | status `cancelled` |
| 2 | `postponed` | status `postponed` |
| 3 | `delayed` | status `delayed`: held, timing unknown, expected to resume (a rain delay, a launch hold) |
| 4 | `overdue` | a commitment (kind `commitment`, in a private namespace) whose deadline has passed and which is not `completed` |
| 5 | `ended` | status `completed`, or the clock has passed the end |
| 6 | `released` | rule 7 holds and the event is an instant (a game or product release) |
| 7 | `live` | status `live`, or the clock has passed the start (a commitment: status `live` only) |
| 8 | `date_confirmed` | the start is known to the day or finer, and its confidence is `confirmed` |
| 9 | `scheduled` | the start is known to the day or finer |
| 10 | `date_expected` | dated, but coarser than a day (a month, a quarter) |
| 11 | `announced` | everything else: no date yet |

`delayed` is only ever asserted by a source or the producer; CastHQ never
infers it from a start time that passed. A delay does not move the schedule,
so a held launch past its start is `delayed` with phase `started`. To infer a
delay for yourself, wait on
`{"all": [{"time": {"anchor": "start", "offset": "PT0S"}}, {"not": {"status": {"in": ["live", "delayed"]}}}]}`.

## Ids: URNs

Every event, topic, change and claim has a URN that never changes:
`urn:casthq:event:<id>`, `urn:casthq:topic:<id>`, `urn:casthq:change:chg_<n>`,
`urn:casthq:claim:<id>`. It resolves at `https://casthq.co/id/<urn>`:

- the canonical URN redirects (303) to the page, or to the API resource;
- a slug, an old id or a merged event redirects (301) to the canonical URN;
- an id withdrawn from the public record answers 410 with a tombstone
  (`error.code` `gone`), never a 404;
- only something that was never public answers 404.

Schema: [/v1/urn.json](/v1/urn.json).
