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.
The four moments#
| Moment | What happens | Credential |
|---|---|---|
| List what is on sale | GET /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 order | POST /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 customer | The 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 back | GET /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"
}| Claim | Required | What it must be |
|---|---|---|
iss | Yes | The channel's partner identity issuer, customerFacing.partnerIdentity.issuer. |
aud | Yes | The 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. |
iat | Yes | When 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. |
jti | Yes | A unique id. Each one is accepted once per channel, so a new assertion is minted for every exchange. |
email | Yes | The buyer's email. |
sub | No | The 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:
| Status | Code | Why |
|---|---|---|
401 | INVALID_PARTNER_ASSERTION | Not a JWT, not signed by the channel's key with its algorithm, or iss is not the channel's issuer. |
401 | PARTNER_ASSERTION_AUDIENCE_MISMATCH | aud is missing or names another channel. |
401 | PARTNER_ASSERTION_EXPIRED | More than five minutes since iat, or past exp. |
401 | PARTNER_ASSERTION_REPLAYED | This jti has already been exchanged on this channel. |
401 | PARTNER_ASSERTION_CLAIM_INVALID | A required claim is absent or malformed; the message names it. |
409 | PARTNER_ASSERTION_EMAIL_CONFLICT | The 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.