Skip to content

Sales channels

Create sales channels, apply versioned allocations, preview an audience, pause, archive with a destination, and manage hosted access links.

Updated View as Markdown

Channels are allocation labels on an event’s inventory. Read sales channels first for the model; this page is the wire reference.

Two capabilities gate everything here, and neither implies the other:

Capability Allows
event:channels:view List, allocation map, preview
event:channels:manage Create, rename, assign, pause, archive, access links

event:block grants neither. Manage tokens minted before channels existed keep exactly the capabilities they had.

List channels

GET/v1/events/:key/channelsSecret key, dashboard session, or event:channels:view manage token

Add ?includeArchived=1 to include archived channels.

{
  "assignmentVersion": 1,
  "publicSale": {
    "id": "public",
    "name": "Public sale",
    "state": "active",
    "counts": { "allocated": 5, "free": 5, "held": 0, "booked": 0, "blocked": 0, "units": 5 }
  },
  "channels": [
    {
      "id": "chn_9f1c", "name": "Travel Agency A",
      "color": "#7C5CFF", "marker": "diagonal", "externalRef": "ta-a",
      "state": "active", "archiveDestination": null, "accessIntent": "none",
      "createdAt": 1764600000000, "updatedAt": 1764600000000, "archivedAt": null,
      "counts": { "allocated": 3, "free": 3, "held": 0, "booked": 0, "blocked": 0, "units": 3 },
      "access": { "intent": "none", "hasActiveGrants": false, "lastMintAt": null }
    }
  ]
}

counts are guest-weighted, so a grouped table contributes its guests; units is the number of inventory rows behind them. allocated is free + held + booked + blocked.

state is active, paused, or archived.

The access object

accessIntent is what you declared; the two derived fields are read from the grant store, so a row can distinguish “server integration configured” from “server integration declared, nothing ever minted”.

Field Meaning
intent none, internal, server, or hosted_link — your declaration, nothing more
hasActiveGrants At least one unexpired, unrevoked buyer session names this channel
lastMintAt Most recent mint for this channel, active or not. null if never

accessIntent grants nothing. What a caller may select is derived from the credential presented, never from this field.

Create a channel

POST/v1/events/:key/channelsSecret key, dashboard session, or event:channels:manage manage token
Field Type Required Notes
name string Yes Unique per event, case-insensitive. Truncated at 80 characters
color string No Organizer-facing only; never reaches a buyer
marker string No Organizer-facing only
externalRef string No Your own stable id. This is the reconciliation key on reports and webhooks
accessIntent string No none (default), internal, server, hosted_link
reason string No Recorded in the channel audit trail

Returns 201 {"ok": true, "channel": {…}} with zeroed counts. Creating a channel does not move assignmentVersion — no inventory changed hands.

Public sale and public are reserved names and return 409 channel_name_taken.

Rename, or change the declared access method

PATCH/v1/events/:key/channels/:channelIdSecret key, dashboard session, or event:channels:manage manage token

Send name, accessIntent, or both. Sending neither returns 422 channel_name_required. Returns {"ok": true, "channel": {…}} with the bare record — no counts, no access sub-object.

Neither operation moves assignmentVersion.

Apply an allocation

POST/v1/events/:key/channels/assignmentsSecret key, dashboard session, or event:channels:manage manage token

This is the operation worth reading carefully. It is versioned, non-destructive, and reports exactly what it did.

{
  "targetChannelId": "chn_9f1c",
  "labels": ["A-1", "A-2", "A-3"],
  "assignmentVersion": 1
}
Field Type Required Notes
labels string[] Yes De-duplicated server-side. At most 5,000 units per request
assignmentVersion integer Yes The version you last read. Not optional, and not decoration — see below
targetChannelId string | null No null or "public" both mean Public sale. Defaults to Public sale
reason string No Recorded in the audit trail

assignmentVersion is concurrency control

assignmentVersion is a counter that moves only when an assignment lands — an Apply that changed something, or an archive. A busy on-sale moves the event’s updatedAt constantly and never touches this, so your operator’s staged edit does not go stale just because tickets are selling.

Send the version you read. If someone else changed the allocation in between, nothing moves and you get:

{
  "error": "channel_assignment_conflict",
  "code": "channel_assignment_conflict",
  "message": "The channel allocation changed after it was read; refresh and review",
  "details": { "assignmentVersion": 2, "expectedAssignmentVersion": 1 }
}

409. details.assignmentVersion is the server’s current value — re-read, show your operator what changed, and retry. Do not retry automatically with the returned version: that turns a safety mechanism into a silent overwrite of a colleague’s work.

The six buckets

{
  "ok": true,
  "targetChannelId": "chn_9f1c",
  "assignmentVersion": 2,
  "requested": 6,
  "applied": 3,
  "buckets": {
    "changedFromPublic":     { "count": 2 },
    "movedFromOtherChannel": {
      "count": 1,
      "channels": [ { "channelId": "chn_4a7b", "name": "Sponsor guests", "count": 1 } ]
    },
    "alreadyInTarget": { "count": 1 },
    "skippedHeld":     { "count": 1, "labels": ["A-7"], "truncated": false },
    "skippedBooked":   { "count": 0, "labels": [], "truncated": false },
    "notFound":        { "count": 1, "labels": ["ZZ-9"], "truncated": false }
  }
}

Every requested label lands in exactly one bucket.

Bucket Meaning
changedFromPublic Moved out of Public sale
movedFromOtherChannel Moved out of another channel, itemised by source
alreadyInTarget Already there. Not an error
skippedHeld A buyer is mid-checkout. Not moved
skippedBooked Already sold. Not moved
notFound Not a label in this event
  • requested counts labels after de-duplication.
  • applied = changedFromPublic.count + movedFromOtherChannel.count. Only free and blocked inventory is ever written.
  • Seats in someone’s checkout and seats already sold are never moved. That is the non-destructive guarantee, and it is why an Apply is safe on a live event.
  • movedFromOtherChannel.channels[].name is null when the source channel row is gone.
  • The three sampled buckets list at most 50 labels; truncated: true means the count exceeded the sample.
  • When applied is 0, assignmentVersion comes back unchanged — an all-skipped Apply does not invalidate the version you are holding.

422 too_many_units carries details: {requested, maximum} when a request exceeds 5,000 units.

Read the allocation map

GET/v1/events/:key/channels/allocationSecret key, dashboard session, or event:channels:view manage token

Label to channel, paged by label. Carries no hold, booking, price, or buyer data.

Query Notes
afterLabel Exclusive cursor. Pass the previous page’s nextAfterLabel
limit Default 1,000, capped at 5,000
{
  "assignmentVersion": 1,
  "allocations": [
    { "label": "A-1", "channelId": "chn_9f1c" },
    { "label": "A-4", "channelId": "public" }
  ],
  "nextAfterLabel": "A-4"
}

nextAfterLabel is null on the last page.

Preview an audience

GET/v1/events/:key/channels/previewSecret key, dashboard session, or event:channels:view manage token

Returns the seat projection a buyer with that exact scope would receive — the same code path a real buyer session uses, not an approximation. Read-only, and it mints nothing.

Query Notes
channelIds Comma-separated. The literal public is read as includePublic, not as a channel
includePublic 1/true. When omitted, defaults to true only if you named no real channels
{
  "ok": true,
  "audience": { "channelIds": ["chn_9f1c"], "includePublic": false },
  "available": true,
  "seats": { "A-1": "free", "A-3": "blocked" },
  "hidden": [], "closed": [],
  "updatedAt": 1764600000000,
  "assignmentVersion": 1
}

A paused, archived, or unknown channel answers with the real unavailable landing state rather than its seats — still 200:

{
  "ok": true,
  "audience": { "channelIds": ["chn_ghost"], "includePublic": false },
  "available": false,
  "unavailable": [ { "channelId": "chn_ghost", "state": "not_found" } ],
  "assignmentVersion": 1
}

Naming more than 20 channels, or an empty scope, returns 422 invalid_channel_scope.

Use this before a partner goes live: it is the check that the allocation you applied is the inventory the partner will actually see.

Pause and resume

POST/v1/events/:key/channels/:channelId/pauseSecret key, dashboard session, or event:channels:manage manage token
POST/v1/events/:key/channels/:channelId/unpauseSecret key, dashboard session, or event:channels:manage manage token

Pausing stops new access and new holds. Existing holds run to their normal expiry — pausing never strands a buyer mid-checkout. Optional reason is recorded. Returns {"ok": true, "channel": {…}}.

Neither moves assignmentVersion.

Archive with a destination

POST/v1/events/:key/channels/:channelId/archiveSecret key, dashboard session, or event:channels:manage manage token

Archiving closes a channel for good and says where its inventory goes.

Field Type Required Notes
destination string | null Yes, as a key null or "public" means Public sale. Otherwise an active channel that is not this one
reason string No Recorded in the audit trail

Archive takes no assignmentVersion. It is unconditional and returns the new value.

{
  "ok": true,
  "channel": {
    "id": "chn_9f1c", "name": "Travel Agency A", "state": "archived",
    "archiveDestination": "public", "archivedAt": 1764600001000
  },
  "assignmentVersion": 2,
  "moved": { "free": 2, "blocked": 0, "booked": 0, "units": 2 },
  "revokedSessions": 1
}
  • revokedSessions counts buyer access sessions cascade-revoked in the same operation. An archived channel never leaves live access behind it.
  • Sold seats keep the channel that sold them, forever. Archiving updates only where a seat would return to if cancelled later. Nothing is deleted, and the channel report still attributes those sales correctly.

Archive refuses to strand a buyer

If anyone is mid-checkout on the channel’s inventory, archive is blocked and tells you when to try again:

{
  "error": "channel_archive_blocked_by_holds",
  "code": "channel_archive_blocked_by_holds",
  "message": "This channel still has active holds; archive is blocked until they release or expire",
  "details": {
    "channelId": "chn_9f1c",
    "activeHolds": 1,
    "heldUnits": 1,
    "latestHoldExpiresAt": 1764600180000,
    "retryAfterMs": 179000
  }
}

409. Wait retryAfterMs and retry, or release the holds first.

A hosted access link is a shareable URL a buyer opens to obtain a buyer access session for one channel, without your backend authenticating them individually. Use one when you cannot authenticate each buyer yourself — a sponsor’s guest list, a press allocation — and a buyer access session when you can.

POST/v1/events/:key/channels/:channelId/access-linksSecret key, dashboard session, or event:channels:manage manage token
Field Type Default Bounds
label string null 80 characters
expiresAt integer (epoch ms) The event’s start, or 7 days out 60 s to 180 days from now
maxRedemptions integer 100 1 to 10,000
maxQuantity integer 4 1 to 100, guest-weighted across the buyer’s holds
sessionTtlSeconds integer 1800 60 to 43,200
includePublic boolean false

expiresAt is an absolute timestamp, not a duration.

{
  "link": {
    "id": "alk_5b0e", "channelId": "chn_9f1c", "label": "VIP list Nov 14",
    "includePublic": false, "expiresAt": 1765200000000,
    "maxRedemptions": 100, "redemptions": 0, "maxQuantity": 4,
    "sessionTtlSeconds": 1800,
    "state": "active", "status": "active",
    "createdAt": 1764600000000, "createdBy": "user:usr_123",
    "revokedAt": null, "lastRedeemedAt": null,
    "rotatedFrom": null, "rotatedTo": null
  },
  "url": "https://app.seatlayer.io/a#alc_ZXZ0X2FiYw_7c1e",
  "capability": "alc_ZXZ0X2FiYw_7c1e",
  "revealedOnce": true
}

state is active, revoked, or rotated; status adds the derived expired and exhausted.

List, rotate, revoke

GET/v1/events/:key/channels/:channelId/access-linksSecret key, dashboard session, or event:channels:view manage token

Status only — {"links": [{…, "activeSessions": 0}]}. No capability, no hash. There is no route that recovers a lost link.

POST/v1/events/:key/channels/:channelId/access-links/:linkId/rotateSecret key, dashboard session, or event:channels:manage manage token

Rotation is how you recover a lost link. It issues a successor inheriting every policy field and ends the predecessor. endActiveSessions is a required boolean — there is no default, because the two answers are very different:

endActiveSessions Effect
false Sessions already minted from the old link keep working until they expire. Use this when the link was merely misplaced
true Those sessions are cascade-revoked before the rotation is acknowledged. Use this when the link leaked

The cascade is scoped to that link, so a session your backend minted directly for the same channel is untouched. The response adds previous and endedSessions to the create shape.

DELETE/v1/events/:key/channels/:channelId/access-links/:linkIdSecret key, dashboard session, or event:channels:manage manage token

Add ?endActiveSessions=1 to cascade. The default is the gentle branch: the link stops admitting new buyers, existing sessions expire naturally.

Errors

Status Code Meaning
404 not_found Unknown event, channel, or link — also used for cross-tenant references
409 channel_name_taken Name already used in this event
409 channel_archived The channel is archived; nothing may change
409 channel_assignment_conflict Stale assignmentVersion. Refresh and review
409 channel_archive_blocked_by_holds Active holds. details says how many and until when
409 channel_assignment_would_drop A chart update would remove allocated inventory. Acknowledge and retry
409 channel_unavailable The channel is not active, so a link cannot be created
409 access_link_not_active The link is revoked, rotated, or otherwise not live
409 too_many_access_links 20 live links on this channel
422 channel_name_required Missing or blank name
422 invalid_channel_destination Archive destination is missing, unknown, archived, or the channel itself
422 invalid_access_intent Not one of the four declared access methods
422 too_many_units More than 5,000 units in one Apply
422 labels_required No usable labels
422 channel_state_invalid The requested state transition is not allowed
422 invalid_channel_scope Empty scope, or more than 20 channels
422 assignment_version_required assignmentVersion absent or not a non-negative integer
422 end_active_sessions_required Rotation did not say what to do with live sessions

Checklist

  • Read assignmentVersion immediately before an Apply, and surface conflicts to a human.
  • Treat alreadyInTarget as success, not as a skip.
  • Set externalRef on every channel you will reconcile against.
  • Preview the audience before a partner goes live.
  • Persist a hosted link’s url at the moment of creation.
  • Choose endActiveSessions deliberately — misplaced and leaked are different.
  • Never treat a channel id from a client as authorization.

Next: buyer access sessions and the private and partner sales tutorial.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close