Skip to content
Docs
Concept

A partner's own server-side storefront

A WordPress site, an Auth0 application or a bespoke storefront sells through Live under a channel secret key from its own server, and signs its already-logged-in user in to Ticketlayer with a partner identity assertion: no second login, no shared password.

A partner runs their own site with their own users and wants to sell your tickets from it: a WordPress plugin, an application behind Auth0, a custom storefront. The server talks to the Live API, exactly as a page or an app does, only with a stronger key. The Backstage API serves the same operations to Live alone, as its upstream, so Live is always in the path.

The credential. The partner's server holds the channel's secret key (tlsk_, medium trust), issued for one channel and never sent to a browser. The channel is the key's, so nothing names it. To sign a buyer in, the partner's own identity provider signs a short-lived partner identity assertion, which Live exchanges for a customer token and keeps in the server's Live session. Both are carried on top of the secret key; neither authenticates a request.

A partner's own server-side storefront: a secret key sells through Live, a partner identity assertion signs the buyer in1 List what is on sale: Partner server to Live: GET /sales/listings (tlsk_, channel: the key's); Live to Backstage API: GET /internal/live/listings (Live's service token, tlsk_); Backstage API to Partner server: the channel's events, in their sale windows, at its prices. 2 Make an order: Partner server to Live: POST /sales/carts, items, POST /sales/carts/{id}/checkout (tlsk_, X-Live-Session); Partner server to Live: POST /sales/orders/{id}/payments (tlsk_, X-Live-Session); Live to Backstage API: the same operations under /internal/live (Live's service token, tlsk_); Backstage API to Partner server: the order. 3 Authenticate a customer: The buyer is already signed in to your site (a WordPress session, Auth0, your own login). There is no Ticketlayer login and no shared password.; Partner server to Partner IdP: sign a JWT, at most five minutes old: iss, aud = the channel id (sch_...), iat, jti (used once), email, optional sub (your IdP's private key); Partner IdP to Partner server: the assertion (partner assertion); Partner server to Live: POST /sales/customer-tokens { partner_assertion } (tlsk_, X-Live-Session, partner assertion); Live to Backstage API: POST /internal/live/customer-tokens (Live's service token, tlsk_); Verified against the channel's issuer, public key and algorithm. aud must name this channel; each jti is accepted once. sub, when sent, is matched before the email; an email held under another external id is a 409, never a merge.; Backstage API to Live: customer token, 30 minutes (customer token); Live keeps the customer token in the session. Identity is exchanged, never asserted.. 4 Read the order back: Partner server to Live: GET /sales/my-orders (tlsk_, X-Live-Session); Partner server to Live: GET /sales/orders/{id} (tlsk_, channel: the key's); Scoped to the key's channel. Another channel's order is a 404, not a 403.1 List what is on sale2 Make an order3 Authenticate a customer4 Read the order backPartner serverPartner IdPLiveBackstage APIThe buyer is already signed in to your site (a WordPress session, Auth0, your own login).There is no Ticketlayer login and no shared password.Verified against the channel's issuer, public key and algorithm.aud must name this channel; each jti is accepted once.sub, when sent, is matched before the email; an email heldunder another external id is a 409, never a merge.Live keeps the customer token in the session.Identity is exchanged, never asserted.Scoped to the key's channel.Another channel's order is a 404, not a 403.GET /sales/listingstlsk_channel: the key'sGET /internal/live/listingsLive's service tokentlsk_the channel's events, in their sale windows, at its pricesPOST /sales/carts, items, POST /sales/carts/{id}/checkouttlsk_X-Live-SessionPOST /sales/orders/{id}/paymentstlsk_X-Live-Sessionthe same operations under /internal/liveLive's service tokentlsk_the ordersign a JWT, at most five minutes old:iss, aud = the channel id (sch_...), iat,jti (used once), email, optional subyour IdP's private keythe assertionpartner assertionPOST /sales/customer-tokens { partner_assertion }tlsk_X-Live-Sessionpartner assertionPOST /internal/live/customer-tokensLive's service tokentlsk_customer token, 30 minutescustomer tokenGET /sales/my-orderstlsk_X-Live-SessionGET /sales/orders/{id}tlsk_channel: the key'sMedium trust: secret keyHigh trust: staff, organisation key, deviceWhere the channel comes fromCarried on top: escalates, never authenticatesNot a credential

The four moments#

MomentWhat happensCredential
List what is on saleGET /sales/listings to Live under the secret key returns what the channel sells, inside its sale windows, at the channel's resolved prices.tlsk_; the channel is the key's
Make an orderPOST /sales/carts, then POST /sales/carts/{id}/items, POST /sales/carts/{id}/checkout, then POST /sales/orders/{id}/payments. The server starts a Live session with its key (POST /session with { channelKey }) and sends the session id back as X-Live-Session.tlsk_, plus the session id
Authenticate a customerThe partner's IdP signs a JWT carrying the claims below. POST /sales/customer-tokens with { type: "partner_assertion", assertion } verifies it against the channel's configured issuer, public key and algorithm, and Live keeps the customer token in the session.tlsk_ authenticates; the assertion, then the customer token, are carried on top
Read the order backGET /sales/my-orders under the escalated session, or GET /sales/orders/{id} by id under the secret key, scoped to its channel.tlsk_, plus the customer token carried on top

The partner identity assertion#

The partner never asks the buyer to log in to Ticketlayer. The channel carries a customerFacing.partnerIdentity block - an issuer, a publicKeyPem and an algorithm - set when the channel is configured. The partner's identity provider signs a JWT with its private key:

{
  "iss": "https://partner.example.com",
  "aud": "sch_...",
  "iat": 1780000000,
  "exp": 1780000300,
  "jti": "5f0c1b9e-3a57-4d8e-9c1a-2b7f6e4d8a10",
  "email": "buyer@example.com",
  "sub": "member-48213"
}
ClaimRequiredWhat it must be
issYesThe channel's partner identity issuer, customerFacing.partnerIdentity.issuer.
audYesThe id of the channel whose key exchanges it, sch_..., not its slug. An assertion minted for one channel is refused on another, even one that trusts the same key.
iatYesWhen it was signed. An assertion more than five minutes old is refused, and so is one past its exp if it carries one. Mint it immediately before the exchange.
jtiYesA unique id. Each one is accepted once per channel, so a new assertion is minted for every exchange.
emailYesThe buyer's email.
subNoThe partner's own stable id for the buyer. It is stored as the customer's externalId and matched before the email.

String claims are at most 255 characters. The server exchanges the assertion with the customer tokens operation, sending its Live session id:

curl https://live.ticketlayer.com/sales/customer-tokens \
  -H "Authorization: Bearer tlsk_..." \
  -H "X-Live-Session: <session id>" \
  -H "Content-Type: application/json" \
  -d '{"proof":{"type":"partner_assertion","assertion":"<jwt>"}}'

The customer token is bound to this channel, good for thirty minutes with a 24 hour absolute ceiling that a refresh carries forward, and Live keeps it in the session. Identity is exchanged, never asserted: a body that names an email without a verified assertion is refused. Auth0, a WordPress session and a bespoke login all work the same way, because the only thing the API needs is a signature it can verify against the key the channel was configured with.

Matching the buyer. With a sub, the customer who already holds that external id is the buyer. Without a match, the email decides: a customer with that email and no external id adopts the sub, and one with that email under a different external id is another of your members, so the exchange is refused with 409 PARTNER_ASSERTION_EMAIL_CONFLICT rather than logging one member in as the other. Two members sharing one address is the usual cause; give each their own email, or correct the external id you assert.

When the exchange is refused. Each refusal names what to fix:

StatusCodeWhy
401INVALID_PARTNER_ASSERTIONNot a JWT, not signed by the channel's key with its algorithm, or iss is not the channel's issuer.
401PARTNER_ASSERTION_AUDIENCE_MISMATCHaud is missing or names another channel.
401PARTNER_ASSERTION_EXPIREDMore than five minutes since iat, or past exp.
401PARTNER_ASSERTION_REPLAYEDThis jti has already been exchanged on this channel.
401PARTNER_ASSERTION_CLAIM_INVALIDA required claim is absent or malformed; the message names it.
409PARTNER_ASSERTION_EMAIL_CONFLICTThe email belongs to a customer with a different external id.

Why a secret key and not an organisation API key#

A secret key reaches what a storefront needs and nothing else: the channel-scoped reads, carts, checkout, payments, the channel's own orders (GET /sales/orders/{id} for another channel's order is a 404, so existence does not leak) and the customer records it creates (POST /sales/customers, GET and PATCH /sales/customers/{id}) - never the whole customer directory, never a catalogue mutation, never keys, reports, the ledger or billing. That ceiling is chosen so that the full domain shapes it receives are safe to hand to a partner. An organisation API key is for your own server administering your own organisation.

The two channel keys differ by where they may be held: a publishable key may sit in a browser, a secret key may not. Both are issued with Issue channel key and shown once.