pass.holder_moved
A pass holder was moved from a cancelled access window to the next available one on the same plan.
When this fires
One holder of a cancelled access window was moved onto another date. Fires once per resettled
holder of an ordinary pass, alongside the single
pass.window_cancelled for the window itself. A season-ticket holder's
move emits pass_series.leg_substituted
instead — the two are mutually exclusive.
The replacement is the soonest window on the same plan that is still on sale, has not started yet, and that this customer does not already hold a pass for. If the schedule has nothing future left, a window already in progress is used rather than stranding them. Every window on a plan sells for the same price, which is what makes the move an even swap rather than a substitution.
They are not charged again. If the cancelled window had not opened yet, their existing invite links stay valid for the replacement date; if they had already been admitted to a live window, that access and those links are revoked immediately and fresh links are sent when the replacement window opens.
Required ability
project-subscription:view — token must carry this at mint time to subscribe an endpoint to this event.
Payload
{
"id": "evt_01HX...",
"type": "pass.holder_moved",
"created_at": "2026-09-18T09:14:22Z",
"api_version": "2026-05-01",
"project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13",
"data": {
"subscription_id": "5b7e2d40-1a86-4c39-97f2-e83d0b16c5a4",
"subscriber_id": "2a91c4e7-6f38-4b52-8e0d-9c1a7b3f5d80",
"plan_id": "c4e82f16-93a7-4d5b-b81c-6e0f27a94d3b",
"from_window_id": "3d5a8c72-b016-4e94-8fa7-61c209d4e738",
"from_starts_at": "2026-09-20T13:00:00+00:00",
"from_ends_at": "2026-09-20T19:00:00+00:00",
"to_window_id": "9a41f0be-27d5-4c18-b3ea-70d5c8194ef2",
"to_starts_at": "2026-09-27T13:00:00+00:00",
"to_ends_at": "2026-09-27T19:00:00+00:00",
"was_queued": true
}
}Field reference
| Field | Type | Notes |
|---|---|---|
id | string ULID | Unique event id, prefixed evt_. Use for idempotent processing. |
type | string | Always pass.holder_moved for this event. |
created_at | ISO 8601 timestamp | Server-side emission time. |
api_version | string | Webhook API contract version. |
project_id | string UUID | Project this event belongs to. |
data.subscription_id | string UUID | The pass purchase that moved. |
data.subscriber_id | string UUID | The subscriber holding it. |
data.plan_id | string UUID | The pass plan. Unchanged by the move. |
data.from_window_id | string UUID | The cancelled window. |
data.from_starts_at | ISO 8601 timestamp | Cancelled window start, always UTC. |
data.from_ends_at | ISO 8601 timestamp | Cancelled window end, always UTC. |
data.to_window_id | string UUID | The window they now hold. |
data.to_starts_at | ISO 8601 timestamp | New window start, always UTC. |
data.to_ends_at | ISO 8601 timestamp | New window end, always UTC. |
data.was_queued | boolean | true if they had already tapped their invite link, so the move asks nothing of them. |
Caveats
- Timestamps are UTC — convert to the plan's schedule timezone before showing them.
was_queuedis the field worth branching on. A queued holder keeps their place and is admitted automatically on the new date; an unqueued one still has to tap their link, and is sent it again as a button.- Nobody is moved onto a date they already hold. A customer with passes for two consecutive windows skips to the one after.
- The move is written quietly with
saveQuietly(), so nosubscription.*event accompanies it. This event is the only signal. - Holders who were paused, past due, or had cancelled their own pass are not moved and emit nothing at all.
Related events
pass.window_cancelled— the window-level event, fired once.pass.holder_stranded— the other outcome, when no window was available.- Pass events overview — back to family overview.
How is this guide?