@sceneinfrastructure/storefront-api
v0.20.0
Published
Typed SDK and oRPC contract for the Mesh public Storefront API
Maintainers
Readme
@sceneinfrastructure/storefront-api
Typed SDK and oRPC contract for Mesh public Storefront API integrations.
pnpm add @sceneinfrastructure/storefront-api @sceneinfrastructure/public-types @orpc/client @orpc/contract @orpc/openapi-client zodimport { createStorefrontApiClient } from '@sceneinfrastructure/storefront-api/client'
const storefront = createStorefrontApiClient({
baseUrl: process.env.MESH_PUBLIC_API_BASE_URL!,
apiKey: process.env.MESH_SCENE_API_KEY!,
})
const { events } = await storefront.listEvents({
sceneId: process.env.MESH_SCENE_ID!,
})
const cart = await storefront.createCartUrl({
eventId: events[0].id,
items: [{ saleKeyId: '0x…', quantity: 2 }],
destination: 'checkout',
})
// Redirect from your backend to skip Mesh ticket selection and open the
// populated hosted checkout form directly.
return cart.absoluteCheckoutUrllistEvents can also narrow the scene event feed server-side:
const { events } = await storefront.listEvents({
sceneId: process.env.MESH_SCENE_ID!,
status: 'scheduled',
eventIds: ['evt_example'],
eventType: 'RSVP',
collaboratorGroupIds: ['scg_partner_team'],
})eventIds and collaboratorGroupIds match any ID in the provided list, and
all provided filters are combined. Collaborator group filtering is scoped to
the requested sceneId.
The optional @sceneinfrastructure/storefront-api/react-query subpath uses
@orpc/tanstack-query. Install it only if you want oRPC's TanStack Query
helpers; otherwise wrap the typed client with your own useQuery calls.
Partner scene and event management
Mesh-issued partner keys use the pt_ prefix and are server-only. Keep them in
the partner backend alongside secret sk_ keys; never expose them to browser
code. A partner key can create, read, and update only scenes linked to its
durable partner identity. It can also manage events and ticket inventory under
those scenes. Partner ownership is checked on every request; knowing another
partner's Scene, event, or tier ID does not grant access.
Event reads use the existing scenes.read partner capability and event or tier
mutations use scenes.write, so already-issued partner keys do not need to be
replaced for this API surface.
Attendee lists contain personal data and require the separate attendees.read
capability. A partner key issued before that capability was available must be
replaced before it can read attendee data.
Ticket validation and check-in require the separate check-ins.write
capability. A partner key issued before that capability was available must be
replaced before it can validate or check in tickets.
Order creation and payment confirmation require orders.write; reading an
order requires orders.read because the response contains customer PII. Mesh
checks the event's durable partner ownership on every order request, so the
same partner key works across owned restaurants without per-Scene keys. A
partner key issued before these capabilities were available must be granted
them or replaced before it can use the order API.
The Mesh super-admin sets a positive scene limit when issuing the partner's first key. Additional keys must use that same limit.
Mutations can include the external operator's stable identifier in
X-Partner-Operator-ID. Mesh stores that unverified operator claim alongside
the authenticated key and request identifiers for audit attribution. Scene
creation also requires a stable, partner-scoped external reference, such as a
venue ID from the partner's system:
const mesh = createStorefrontApiClient({
baseUrl: process.env.MESH_PUBLIC_API_BASE_URL!,
apiKey: process.env.MESH_PARTNER_API_KEY!,
})
const { scene } = await mesh.createScene({
body: {
externalReference: 'venue-123',
name: 'Example Venue',
avatarUrl: 'https://cdn.example.com/restaurant.png',
},
headers: { 'x-partner-operator-id': 'operator-7' },
})
const current = await mesh.getScene({
params: { sceneId: scene.id },
})
await mesh.updateScene({
body: { name: 'Example Venue Downtown' },
headers: { 'x-partner-operator-id': 'operator-8' },
params: { sceneId: scene.id },
})
const { event, tiers } = await mesh.createEvent({
body: {
eventType: 'TICKETED',
name: 'Summer Dinner',
startAt: '2026-09-01T19:00:00.000Z',
endAt: '2026-09-01T22:00:00.000Z',
taxEnabled: true,
taxRateBps: 887,
timezone: 'America/New_York',
tiers: [
{
label: 'General admission',
priceUsdc: '50000000',
totalQuantity: '100',
},
],
},
headers: {
'idempotency-key': stableEventCreateKey,
'x-partner-operator-id': operatorId,
},
params: { sceneId: scene.id },
})
await mesh.updateEventAvailability({
body: { acceptingRegistration: true, visibility: 'public' },
headers: { 'x-partner-operator-id': operatorId },
params: { eventId: event.id },
})
await mesh.publishEvent({
headers: { 'x-partner-operator-id': operatorId },
params: { eventId: event.id },
})
// After polling getEvent until state is "published":
const order = await mesh.createOrder({
body: {
payment: { provider: 'blackbird_pay', type: 'external' },
quantity: 1,
slotId: tiers[0].id,
type: 'reserved',
user: {
email: '[email protected]',
firstName: 'Jane',
lastName: 'Doe',
},
},
headers: { 'idempotency-key': stableOrderKey },
params: { eventId: event.id },
})
const attendeePage = await mesh.listEventAttendees({
params: { eventId: event.id },
query: { limit: 50, offset: 0, status: 'all' },
})
const validation = await mesh.validateEventCheckIn({
body: { token: scannedQrToken },
params: { eventId: event.id },
})
const submission = await mesh.checkInEventTickets({
body: {
token: scannedQrToken,
tickets: validation.attendee.tickets
.filter((ticket) => ticket.valid)
.map((ticket) => ({ quantity: 1, tokenId: ticket.tokenId })),
},
headers: {
'idempotency-key': stableCheckInKey,
'x-partner-operator-id': operatorId,
},
params: { eventId: event.id },
})externalReference is unique within the authenticated partner. Retrying a
create with the same reference and same create payload returns the existing
scene. Reusing it with different create data returns a typed 409 conflict.
For a 503, inspect error.data.reason. Retry the same payload and reference
only for scene_provisioning_unavailable; Mesh resumes from durable
provisioning state when it can do so safely. Escalate
partner_service_actor_invalid and wallet_reconciliation_required to Scene
instead of retrying. The latter deliberately requires reconciliation rather
than risking a duplicate wallet or scene.
The HTTP operations are:
POST /v1/scenesGET /v1/scenes/{sceneId}PATCH /v1/scenes/{sceneId}POST /v1/scenes/{sceneId}/eventsGET /v1/scenes/{sceneId}/eventsGET /v1/events/{eventId}PATCH /v1/events/{eventId}PATCH /v1/events/{eventId}/availabilityPOST /v1/events/{eventId}/publishPOST /v1/events/{eventId}/archivePOST /v1/events/{eventId}/tiersPATCH /v1/events/{eventId}/tiers/{tierId}POST /v1/events/{eventId}/tiers/{tierId}/archiveGET /v1/events/{eventId}/attendeesPOST /v1/events/{eventId}/check-ins/validatePOST /v1/events/{eventId}/check-ins
Event and ticket-tier creation require a stable UUID Idempotency-Key so
clients can safely retry a request without creating duplicate inventory.
Publication is asynchronous: a successful publish request returns
publishStatus: "queued"; poll the event read until state is published.
Published event and tier updates use Mesh's existing onchain sale and inventory
synchronization. Tier responses include soldQuantity and remainingQuantity;
quantities and prices are integer USDC base-unit strings so JavaScript clients
do not lose precision.
taxRateBps is an integer from 0 through 10,000 (887 means 8.87%). When tax is
enabled, a non-null value overrides Mesh's location-derived rate; patch it to
null to return to location-derived tax. Partners cannot set or disable
Scene's service fee: partner create and update inputs do not expose
vendorFeesEnabled, and partner-created events always enable the fixed fee.
Attendee reads are paginated and support search, ticket/status filters, and sorting. They return current ticket ownership and check-in state plus the latest successful-order registration answers for each current holder. Answers remain associated with the destination wallet on the order; transferring a ticket does not disclose the original holder's answers to the recipient.
Check-in validation accepts only the opaque, event-bound sct1: token from a
Scene ticket QR; it does not accept a wallet address. Submit only ticket IDs
and quantities returned by validation, and reuse the same UUID
Idempotency-Key when retrying the same logical check-in. A successful
submission returns status: "submitted" and a userOpHash; this means the
onchain operation was accepted for submission, not that it is confirmed.
If a create has an ambiguous outcome, Mesh keeps its idempotency reservation
for 24 hours rather than risking a duplicate. Do not retry the same logical
create under a new key; use the response requestId when escalating to Scene.
What the public read endpoints expose
The public read endpoints only return events that are
state = 'published', visibility = 'public', and not archived. Drafts,
publishing rows, archived rows, and private rows are filtered out at the
read layer.
Each event also carries:
acceptingRegistration: boolean— whenfalse, every tier is reported asisActive: false.display: { headline, dateLabel, shortDateLabel, venueLabel, imageAlt, accentLabel, numberLabel }— storefront-ready labels for cards, event detail headers, and checkout summaries.imageUrl: string | nullandvideoUrl: string | null— separate poster and MP4 media URLs so storefronts can render a stable poster and autoplay video when available.location: { name, address, formattedAddress, kind, url } | null— derived from the V2event.locationJSON when present, with a fallback to V1event.address.
Each tier carries:
display: { group, sortOrder, subtitle, visible, defaultSelected }— admin-defined ticket group label and ordering, a storefront subtitle, visibility, and a default selection hint so storefronts don't need to infer groups or first-choice tiers from labels.price.formattedandallInclusivePrice.formatted— canonical USD display labels alongside numeric USD and integer USDC values.quantity: { label, remainingNumber, ... }— storefront-ready inventory labels and safe numeric remaining quantities when available.relationships: { requiredParentTierId }— when non-null, the tier is an add-on that must be purchased with an eligible primary ticket. Checkout accepts the exact referenced parent tier, except for Mesh's internal "any primary" add-on group where any primary event ticket can satisfy the relationship.restrictions: { maxQuantityPerOrder, requiresAccessCode }— same surface as before.schedule: { startTime, endTime, timezone }andvalidityWindow: { validFrom, validUntil, isActive }— public sale and ticket-validity windows for storefront presentation.
POST /v1/cart-url accepts multi-item carts and supports two destinations:
- Omit
destination(or pass"event") to preserve the original behavior. The returned URL opens the Mesh event page and encodes each selection as repeatedsaleKeyId/quantityquery parameters. - Pass
destination: "checkout"to open the hosted attendee/payment form directly. The URL contains a one-hour encryptedcartTokenthat binds the validated items to the event and reconstructs the cart on initial load and refresh. Access and discount codes are carried inside that opaque token, rather than exposed as checkout query parameters.
checkoutUrl is always the relative URL. absoluteCheckoutUrl contains the
same destination under the hosted Mesh origin, or null when the API runtime
does not have a hosted origin configured.
For either destination, the endpoint:
- Aggregates duplicate
saleKeyIdlines server-side before validation, so splitting an order into multiple identical line items cannot bypass availability or add-on parent-quantity checks. - Sorts primary tiers before add-ons in the emitted URL so the hosted storefront reconstructs parents before children regardless of input ordering.
- Accepts an optional HTTP(S)
returnUrl. After the order is fulfilled, the hosted confirmation page gives the shopper a path back to that exact page. When omitted, confirmation links to the Mesh ticket page instead.
It enforces these rules server-side:
- 409 Conflict when an add-on tier is included without an eligible primary
ticket in the same cart (
requiredParentTierIdnot satisfied by the exact parent tier or Mesh's internal "any primary" add-on group). - 409 Conflict when an add-on tier's quantity exceeds the eligible primary ticket quantity in the same cart.
- 409 Conflict when a tier is sold out or requested quantity exceeds remaining availability (after duplicate-line aggregation).
- 422 Validation Failed when the cart includes more than one distinct primary ticket tier. The current hosted storefront state model supports only one primary tier (with optional add-ons). Pass a single primary tier per cart URL.
- 422 Validation Failed when the sum of primary ticket quantities
exceeds
event.ticketOrderLimit(treated as "no limit" when non-positive). Add-on quantities ride along with their parent and are not counted against the per-event order limit (they are independently capped at the parent quantity).
Access and discount code validation
Code resolution and validation require a secret, server-side scene API key. Never call these operations from browser code or expose the key in a public environment variable.
Resolve an access code before rendering gated inventory:
const resolved = await storefront.resolveAccessCode({
params: { eventId },
body: { accessCode },
})
if (resolved.accessCode.status === 'applied') {
// resolved.tiers includes the storefront-safe gated tiers unlocked by the
// code. The code itself is never echoed in the response.
renderTiers(resolved.tiers)
}Validate either or both codes against the current cart whenever ticket or add-on quantities change:
const validation = await storefront.validateCartCodes({
eventId,
items: [{ saleKeyId, quantity }],
accessCode: accessCode || undefined,
discountCode: discountCode || undefined,
})
if (validation.discountCode.status === 'applied' && validation.pricing) {
showTotal(validation.pricing.total.formatted)
showSavings(validation.discountCode.savings?.formatted)
}Each code has one of these statuses:
applied— accepted for the exact cart.invalid— not recognized for the event.expired— recognized, but its applicable end condition has passed.inactive— recognized, but not currently usable (for example a future window, disabled discount, exhausted usage limit, or closed registration).not_applicable— recognized, but does not apply to the selected items or cart amount.not_supplied— omitted from the request.
validation.valid is true only when every supplied code is applied.
pricing uses the same amountUSD, integer amountUSDC, and formatted
money shape as the catalog endpoints. The operation performs no reservation,
database write, or cart-token issuance. It is rate-limited per authenticated
API key and event; typed 429 errors include retryAfterSeconds.
Validation is advisory and inventory is not held. Always send the same items
and codes to createCartUrl with destination: 'checkout'; Mesh independently
revalidates the cart and places the codes inside the encrypted one-hour cart
token:
const cart = await storefront.createCartUrl({
eventId,
items,
destination: 'checkout',
returnUrl,
accessCode: accessCode || undefined,
discountCode: discountCode || undefined,
})
return cart.absoluteCheckoutUrlReserved and RSVP order workflow
Reserved orders are a server-side checkout alternative for partners that
collect or attest payment outside Mesh. Use them from a trusted backend only;
the scene API key and Idempotency-Key must not be sent from browser code.
The flow is:
POST /v1/events/{eventId}/orderswithIdempotency-Keyto reserve inventory and receive an order inpayment_pendingstatus.- Complete the external payment with the provider.
POST /v1/orders/{orderId}/confirmwith the external payment attestation.GET /v1/orders/{orderId}to poll or reconcile the canonical order status and issued ticket.
Create-order requests include purchaser contact, payment handoff details, an
optional reservation TTL, and either the legacy single-item shape
(slotId/quantity) or a multi-item items array. Each items entry uses the
public slot identifier returned by Mesh plus its quantity. Multi-item reserved
orders reserve, price, and fulfill every line item atomically; add-on tiers must
include an eligible primary ticket in the same order and cannot exceed the
eligible primary quantity. Create, confirm, and get responses include items so partners
can reconcile the exact session/add-on allocation. They also include
cart.breakdown, with subtotal, itemized fees (processingFee,
protocolFee, vendorFee, referrerFee, total), itemized taxes
(salesTax, total), and final total in USD minor units. Confirm requests
require the configured provider identifier, amount, currency, external payment
ID, paid timestamp, and paid status. Confirmed orders return the ticket details
when fulfillment succeeds; orders may also remain fulfillment_pending, fail,
expire, or be fetched later for reconciliation.
For RSVP events with no payment component, use the same create-order endpoint
with type: "rsvp" and omit payment. Mesh validates that the event is an RSVP
event and that the selected slots price to zero, then immediately starts ticket
fulfillment. The create response is the canonical order shape with type:
"rsvp" and a status such as fulfillment_pending or confirmed; poll
GET /v1/orders/{orderId} when fulfillment is pending.
Always send a stable Idempotency-Key for create-order retries. Reusing the
same key for the same logical order is safe; generating a new key for every
retry can create duplicate reservations.
Named request/response types
@sceneinfrastructure/storefront-api exports named TypeScript aliases
for every endpoint so consumers don't need to derive output types from
the client or z.infer on schemas:
import type {
CheckInEventTicketsInput,
CheckInEventTicketsOutput,
ListEventsOutput,
ListEventTiersOutput,
ListEventPricesOutput,
ListEventAttendeesOutput,
CreateCartUrlInput,
CreateCartUrlOutput,
ResolveAccessCodeInput,
ResolveAccessCodeOutput,
ValidateCartCodesInput,
ValidateCartCodesOutput,
ValidateEventCheckInInput,
ValidateEventCheckInOutput,
CreateSceneInput,
CreateSceneOutput,
CreateSceneOutput,
UpdateSceneInput,
UpdateSceneOutput,
CreateOrderInput,
CreateOrderOutput,
ConfirmOrderInput,
ConfirmOrderOutput,
GetOrderOutput,
StorefrontApiEvent,
StorefrontApiEventDisplay,
StorefrontApiEventLocation,
StorefrontApiEventType,
PublicApiProvisionedScene,
StorefrontApiPrice,
StorefrontApiTier,
StorefrontApiTierDisplay,
StorefrontApiTierRelationships,
StorefrontApiTierRestriction,
StorefrontApiOrderItem,
StorefrontApiOrderSlot,
StorefrontApiOrderStatus,
StorefrontApiOrderTicket,
} from '@sceneinfrastructure/storefront-api'The schemas (listEventsOutputSchema, publicApiTierSchema, etc.) and
typed client (@sceneinfrastructure/storefront-api/client) remain
available unchanged.
Agent skill
If you are using an AI coding agent, install the Mesh Storefront skill for API/SDK setup guidance, Next.js server-only usage patterns, required env vars, and OpenAPI fallback examples:
npx skills add https://api.sceneconstruction.xyzThe installer discovers the skill from
https://api.sceneconstruction.xyz/.well-known/agent-skills/index.json.
