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
structured-mode event. The change feed (GET /v1/changes?format=cloudevents)
and every subscription firing carry it. Schema:
/v1/change-envelope.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). 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.codegone), never a 404; - only something that was never public answers 404.
Schema: /v1/urn.json.