npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sceneinfrastructure/storefront-api

v0.25.0

Published

Typed SDK and oRPC contract for the Mesh public Storefront API

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 zod
import { 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.absoluteCheckoutUrl

listEvents 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.

Server-side analytics reports

Analytics report routes expose unified, human-only metrics across Mesh-hosted and custom storefront traffic. They require an analytics.read permission on a server-side sk_ or pt_ key. Never call them from browser code or expose the key to a client. Report ingestion is asynchronous, and revenue values are integer USDC base-unit strings (money.decimals is 6) so they remain precise.

import { createStorefrontApiClient } from '@sceneinfrastructure/storefront-api/client'

const mesh = createStorefrontApiClient({
  baseUrl: process.env.MESH_PUBLIC_API_BASE_URL!,
  apiKey: process.env.MESH_SERVER_API_KEY!,
})

const range = {
  startDate: '2026-09-01T00:00:00.000Z',
  endDate: '2026-09-30T23:59:59.999Z',
}

const eventFunnel = await mesh.getEventAnalytics({
  params: { eventId: 'evt_example' },
  query: { ...range, storefront: 'partner.example' },
})

const eventTiers = await mesh.getEventAnalyticsTiers({
  params: { eventId: 'evt_example' },
  query: range,
})

const sceneBySource = await mesh.getSceneAnalyticsBreakdown({
  params: { sceneId: process.env.MESH_SCENE_ID! },
  query: { ...range, groupBy: 'utm_source', limit: 50 },
})

const grossRevenueBaseUnits = BigInt(
  eventFunnel.conversion.grossRevenueUsdc,
)

Event and Scene scopes each support the funnel, Tiers, and Breakdown methods. Optional filters are country, referrer, storefront, utmCampaign, utmMedium, and utmSource; all supplied filters are combined. Breakdowns require groupBy, while limit defaults to 50.

The HTTP routes are GET /v1/events/{eventId}/analytics and GET /v1/scenes/{sceneId}/analytics, each with optional /tiers and /breakdown suffixes. startDate and endDate are required UTC ISO dates; the start must not follow the end. limit accepts 1–100.

Omit storefront for unified totals across Mesh and every storefront, including backfilled orders. Scene keys need scene:{sceneId}:analytics.read; partner keys need partner:{partnerId}:analytics.read and durable ownership of the event's Scene. Existing keys without that permission must be replaced or explicitly granted it; this API change does not update issued keys.

These are aggregate reports, not visitor histories or order lists. Historical order/revenue backfills do not reconstruct discarded views or attribution. New events and orders appear after asynchronous ingestion, not necessarily immediately after a successful write.

Partner analytics ingestion and handoff

trackAnalyticsEvent, trackServerAnalyticsEvent, and createHandoffToken accept server-only partner keys with partner:{partnerId}:analytics.write. Partner calls must include sceneId; the API verifies active partner ownership before queueing events or minting handoff tokens. scenes.read, scenes.write, and analytics.read do not grant analytics writes.

Existing partner keys need the new capability explicitly granted or must be replaced; this code change does not update issued keys. Keep partner keys on your backend, never in browser analytics configuration. Single-Scene keys can continue to omit sceneId. Publishable Scene keys cannot call server tracking.

await client.trackServerAnalyticsEvent({
  sceneId,
  eventId: crypto.randomUUID(),
  eventName: 'visit.started',
  visitId,
})
const { token } = await client.createHandoffToken({ sceneId, visitId })

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.

Minimum permissions are intentional API policy, not inferred from an all-access key. Scene capabilities below use scene:{sceneId}:; partner capabilities use partner:{partnerId}: and additionally require active durable Scene ownership.

| Management operation | Secret Scene key | Partner key | | --- | --- | --- | | Read a managed event (including drafts/private fields) | events.write | scenes.read | | Create/update/publish/archive events and tiers; create image uploads | events.write | scenes.write | | Read attendees | orders.read | attendees.read | | Validate/check in tickets | events.write | check-ins.write |

Public catalog reads are a different surface from managed-event reads. An events.read Scene key does not grant management access. Neither a valid key nor permission for an event grants access to a tier belonging to another event: missing, deleted, and foreign tiers return 404 before accepting queued work.

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.

Reserved order creation requires orders.write. Payment confirmation requires both orders.write and orders.read for scene and partner keys, because its response contains customer PII; reading an order also requires orders.read. 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: {
    defaultReferrerAddress: '0x…',
    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 coverImage = await readFile('/path/to/event-cover.jpg')
const imageUpload = await mesh.createEventImageUpload({
  body: {
    contentLength: coverImage.byteLength,
    contentType: 'image/jpeg',
  },
  headers: { 'x-partner-operator-id': operatorId },
  params: { sceneId: scene.id },
})

const imageResponse = await fetch(imageUpload.uploadUrl, {
  body: coverImage,
  headers: imageUpload.uploadHeaders,
  method: imageUpload.uploadMethod,
})
if (!imageResponse.ok) throw new Error('Event image upload failed')

const { event, tiers } = await mesh.createEvent({
  body: {
    emailSettings: {
      avatarUrl: imageUpload.imageUrl,
      hostName: 'Propaganda',
      supportEmail: '[email protected]',
    },
    eventType: 'TICKETED',
    imageUrl: imageUpload.imageUrl,
    name: 'Summer Dinner',
    referralFeeBps: 200,
    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.updateEvent({
  body: {
    emailSettings: { supportEmail: '[email protected]' },
  },
  headers: { 'x-partner-operator-id': operatorId },
  params: { eventId: event.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: {
    additionalFee: {
      amountUSDC: '5000000',
      label: 'Restaurant fee',
    },
    payment: { provider: 'blackbird_pay', type: 'external' },
    quantity: 1,
    slotId: tiers[0].id,
    type: 'reserved',
    // Overrides scene.defaultReferrerAddress for this purchase.
    referrerAddress: '0x…',
    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. When that conflict refers to a complete, active Scene, error.data.existingSceneId contains its ID. Incomplete provisioning reservations do not expose an ID.

Published event changes that affect onchain sales (tax/fees, end time, RSVP capacity, ticket tiers, and archive) return an optional operation: { id, status }. pending or running means accepted, not applied: poll getEvent until that same operation ID is completed before reporting success or placing orders. failed means execution stopped before a plan was frozen; review and retry. reconciliation_required requires operator investigation; do not create a replacement operation. A client timeout does not cancel queued work. Conflicting edits and new orders return 409 while an operation is active, and existing pending orders must finish or expire before an onchain edit can start. Synchronous edits omit operation. Initial publishing still uses publishStatus and the event's state rather than a mutation operation UUID.

PATCH fields have three distinct meanings: omitted fields preserve stored values; explicit null clears only fields whose schema permits null; concrete values replace the field. In particular, imageUrl: null removes the cover and additionalDetails: null clears the details. Unrelated fields must remain unchanged. false and allowed zero values (such as taxRateBps: 0) are values, not omissions. Invalid values return a validation error without accepting work.

Queued mutations can apply confirmed tier/sale groups incrementally; event metadata changes only after every group succeeds. A GET may expose the latest operation, but a mutation response identifies only that request's operation: an unrelated historical completion or failure is not its outcome. Tests must assert persisted rows and readback, not just a successful HTTP response.

To recover after losing or failing to persist a create response, resolve the stable external reference, persist the returned scene.id, and use the Scene ID for future reads and updates. Do not resend changed fields to createScene; apply them with PATCH /v1/scenes/{sceneId} instead:

const { scene: recoveredScene } = await mesh.getSceneByExternalReference({
  params: { externalReference: 'venue-123' },
})

await persistMeshSceneId(recoveredScene.id)

await mesh.updateScene({
  body: { name: 'Example Venue Downtown' },
  headers: { 'x-partner-operator-id': 'operator-8' },
  params: { sceneId: recoveredScene.id },
})

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/scenes
  • GET /v1/scenes/by-external-reference/{externalReference}
  • GET /v1/scenes/{sceneId}
  • PATCH /v1/scenes/{sceneId}
  • POST /v1/scenes/{sceneId}/event-image-uploads
  • POST /v1/scenes/{sceneId}/events
  • GET /v1/scenes/{sceneId}/events
  • GET /v1/events/{eventId}
  • PATCH /v1/events/{eventId}
  • PATCH /v1/events/{eventId}/availability
  • POST /v1/events/{eventId}/publish
  • POST /v1/events/{eventId}/archive
  • POST /v1/events/{eventId}/tiers
  • PATCH /v1/events/{eventId}/tiers/{tierId}
  • POST /v1/events/{eventId}/tiers/{tierId}/archive
  • POST /v1/events/{eventId}/discount-codes
  • GET /v1/events/{eventId}/discount-codes
  • PATCH /v1/events/{eventId}/discount-codes/{codeId}
  • DELETE /v1/events/{eventId}/discount-codes/{codeId}
  • GET /v1/events/{eventId}/attendees
  • POST /v1/events/{eventId}/check-ins/validate
  • POST /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.

Event image uploads accept JPEG or PNG files up to 4 MiB. The returned upload URL expires after 10 minutes and is bound to the requested byte length and content type. Each URL permits one processing attempt, including concurrent requests; request a fresh URL after a failed or ambiguous attempt. Processing is limited to 60 attempts per scene and 20 per client IP per minute; a 429 response includes Retry-After. Mesh validates and sanitizes the bytes during the upload. After a successful upload, pass the returned imageUrl to event creation or update. Creating an upload requires the same scenes.write partner capability or events.write scene capability as event mutations.

Discount codes can use a fixed USDC amount or an integer percentage in basis points (2,500 means 25%). Set tierIds to an empty array to apply a code to all tiers, or provide event tier IDs to scope it. minimumOrderAmountUsdc is tested against the full pre-discount order subtotal. Maximum uses and ISO date windows can be null. List and mutation responses include the current uses count. Codes are case-insensitively unique within an event.

const { discountCode } = await mesh.createDiscountCode({
  body: {
    code: 'DINNER25',
    enabled: true,
    maxUses: 100,
    minimumOrderAmountUsdc: '50000000',
    tierIds: [tiers[0].id],
    validFrom: null,
    validUntil: null,
    value: { type: 'percentage', percentageBps: 2_500 },
  },
  headers: { 'x-partner-operator-id': operatorId },
  params: { eventId: event.id },
})

const { discountCodes } = await mesh.listDiscountCodes({
  params: { eventId: event.id },
})

await mesh.updateDiscountCode({
  body: { enabled: false },
  headers: { 'x-partner-operator-id': operatorId },
  params: { eventId: event.id, codeId: discountCode.id },
})

await mesh.deleteDiscountCode({
  headers: { 'x-partner-operator-id': operatorId },
  params: { eventId: event.id, codeId: discountCodes[0].id },
})

Event emailSettings customize the existing Scene email templates for one event. hostName overrides the sender display name and email header, avatarUrl overrides the Scene image, and supportEmail is shown in the help footer and used as the reply-to address. Omitted values inherit Scene branding; patch a value to null to restore that fallback. Partial updates merge with the event's existing email settings, so they do not reset ticket-link, access, SMS, or other internal delivery settings. The event image upload endpoint can also be used for the email avatar. This is event-level branding of the standard templates, not a full custom template swap.

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. referralFeeBps is an additive share from 0 through 9,700 (200 means 2%). It is added on top of the standard 4% service fee (1% protocol plus 3% Scene) and is paid only when the reserved order resolves a nonzero referrer address. Set defaultReferrerAddress on the scene for the normal recipient, or pass referrerAddress on an individual reserved order to override it. Mesh rejects zero-address referrers and referral-fee orders that resolve no recipient. Partners still cannot set or disable Scene's base service fee: vendorFeesEnabled is not exposed, and partner-created events enable it.

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 — when false, every tier is reported as isActive: false.
  • display: { headline, dateLabel, shortDateLabel, venueLabel, imageAlt, accentLabel, numberLabel } — storefront-ready labels for cards, event detail headers, and checkout summaries.
  • imageUrl: string | null and videoUrl: 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 V2 event.location JSON when present, with a fallback to V1 event.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.formatted and allInclusivePrice.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 } and validityWindow: { 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 repeated saleKeyId/quantity query parameters.
  • Pass destination: "checkout" to open the hosted attendee/payment form directly. The URL contains a one-hour encrypted cartToken that 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 saleKeyId lines 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 (requiredParentTierId not 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.absoluteCheckoutUrl

Reserved 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:

  1. POST /v1/events/{eventId}/orders with Idempotency-Key to reserve inventory and receive an order in payment_pending status.
  2. Complete the external payment with the provider.
  3. POST /v1/orders/{orderId}/confirm with the external payment attestation.
  4. GET /v1/orders/{orderId} to poll or reconcile the canonical order status and issued ticket.

Use GET /v1/events/{eventId}/orders?limit=50&offset=0 to build an event admin order list without fetching each order separately. Results are newest first and return { orders, pagination }, where each canonical order includes createdAt. The list contains orders created through this Storefront API, not orders from Mesh-hosted checkout.

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. For access-code-gated tiers, pass body.accessCode on both reserved and RSVP orders, using the same code supplied to resolveAccessCode. Resolving a code does not authorize later requests implicitly. The code is checked again during inventory reservation; missing or incorrect codes keep gated slots unavailable (404 Slot not found). Use the selected tier's saleKeyId as slotId to identify its exact sale and price. A tier ID is accepted only when it identifies one available sale; ambiguous tier IDs return 422 instead of selecting an arbitrary sale variant. 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. Pass discountCode to apply an enabled, valid code to the order. Create, confirm, get, and list responses include a nullable discount with the normalized code, applied amount in USD minor units, and current useCount. Their returned cart contains discounted totals. Responses also include items so partners can reconcile the exact session/add-on allocation, plus 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.

Reserved orders can also include one flat additionalFee. Its amountUSDC is a positive signed-64-bit USDC base-unit string rounded to a whole USD cent; label defaults to "Additional fee". It appears separately as cart.additionalFee, is included in cart.breakdown.fees.total, the external payment amount, and the canonical order total, but is deliberately excluded from onchain ticket settlement. It is a one-time order fee, not a per-ticket fee or percentage.

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,
  ListOrdersInput,
  ListOrdersOutput,
  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.xyz

The installer discovers the skill from https://api.sceneconstruction.xyz/.well-known/agent-skills/index.json.