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

FieldTypeNotes
idstring ULIDUnique event id, prefixed evt_. Use for idempotent processing.
typestringAlways pass.holder_moved for this event.
created_atISO 8601 timestampServer-side emission time.
api_versionstringWebhook API contract version.
project_idstring UUIDProject this event belongs to.
data.subscription_idstring UUIDThe pass purchase that moved.
data.subscriber_idstring UUIDThe subscriber holding it.
data.plan_idstring UUIDThe pass plan. Unchanged by the move.
data.from_window_idstring UUIDThe cancelled window.
data.from_starts_atISO 8601 timestampCancelled window start, always UTC.
data.from_ends_atISO 8601 timestampCancelled window end, always UTC.
data.to_window_idstring UUIDThe window they now hold.
data.to_starts_atISO 8601 timestampNew window start, always UTC.
data.to_ends_atISO 8601 timestampNew window end, always UTC.
data.was_queuedbooleantrue 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_queued is 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 no subscription.* 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.

How is this guide?

On this page

Subscriby is a product designed by you — for you.

No boardroom full of executives deciding what we ships next. Our roadmap always shaped by you with your feedback.

Share feedback or a request