Primitive spec

Trevize

The channel-distribution primitive. Trevize is a generic partner API plus iCal feeds in both directions, so third-party channels — aggregators, marketplaces, plain calendar clients — can read a tenant's availability and book against real Seldon inventory. One disciplined edge where external demand meets the platform, instead of N bespoke integrations.

Note: Named marketplace adapters (Reserve-with-Google / ClassPass-class) are partnership-gated and not yet built — the generic partner API and iCal feeds are the live surfaces those adapters will sit on.

What it owns

Trevize owns the edge where external demand meets bookable supply: partner authentication and scopes, channel configuration, listing policy (overrides, allotments, price), booking idempotency, the external↔internal booking mapping, outbound webhooks, and iCal feeds. A Channel is one distribution relationship; a ChannelListing publishes a Seldon Offer to it; an ExternalBooking maps the channel's booking id to the real Seldon booking and its invoice.

Trevize never computes availability itself — it delegates to Seldon's read model and applies channel policy on top. It does not own the booking lifecycle (Seldon), API keys (Mannix), money (Mallow), or customer identity (Terminus — channel customers merge into the same person graph by email or phone). It composes them behind one machine-facing surface.

Concepts

Channel
One external distribution relationship per tenant (tchn_…). Carries a kind (partner_api or ical), a status (active, paused, revoked), a unique binding to the Mannix ApiKey the partner authenticates with (display prefix surfaced in the console for support), an optional webhook endpoint, and commissionBps — reporting only; Trevize computes owed commission but never moves money.
ChannelListing
A Seldon Offer published to a Channel (tlst_…), unique per (channel, offer). overrides narrow only — display name, lead times, party max; a listing can never widen what the Offer allows. allotment caps (maxConcurrentBookings, maxPerDay) protect direct capacity from being swallowed by a channel. unitPriceCents + currency are the price basis for channel invoices.
ExternalBooking
The mapping row and the heart of idempotency (txbk_…): channel, listing, the channel's externalRef, the real seldonBookingId, and the invoice. A replayed create returns the prior result, never a duplicate booking. Commission is computed at confirm; customer contact details are redacted from the stored payload snapshot.
Availability push (webhooks)
An outbox subscriber watches the events that move availability — booking confirms/cancels/modifies, staff-shift changes, scheme changes — resolves the affected listings, debounces per listing, and enqueues durable WebhookDelivery rows (exponential backoff, five attempts, then dead + a health signal). Deliveries are HMAC-signed (X-Trevize-Signature) and carry a hint, not a payload: partners re-pull the availability endpoint, which keeps consistency simple.
Channel-collected money
v1 channels collect payment themselves. At confirm, Trevize issues a Mallow invoice at the listing price and settles it against a dedicated 1015 Channel Settlement asset account — an honest receivable-from-channel balance until remittance ingest lands. The money step runs inside the booking's transaction (a posting failure rolls the booking and its holds back), and cancels post no automatic reversal — Trevize cannot know whether the channel actually refunded.
iCal, both directions
Export: tokenized feeds (ChannelFeedToken, hashed at rest) render confirmed bookings as an ICS calendar any Google/Apple client can subscribe to; revoking a token kills its URL. Import: an ical-kind Channel subscribes to an external ICS per resource — a Daneel-scheduled trevize.ical_pull diffs events and maintains pinned Trantor busy holds, so time booked on an outside calendar blocks bookable capacity here. A parse error applies zero mutations.
Commission reporting
GET /trevize/v1/reports/commission aggregates channel bookings and owed commission by channel by ISO week; cancelled bookings are excluded from the sum and reported separately. The console's external-bookings ledger shows the same numbers per booking.
Partner-initiated modify
PATCH a booking's partyCount or slotRef with the same revalidation semantics as first-party changes — plus a reprice guard: if the change would alter the invoiced total, the request is rejected (422) and the channel cancels and rebooks, rather than Trevize silently mutating an invoice.
Security hardening
PartnerAuthenticator is the first line of every partner action: the tenant is resolved exclusively from the verified key — headers and body context are structurally unread on partner routes — with per-key rate limiting and sandbox-flagged requests rejected fail-closed. Outbound webhook and ICS fetches go through an SSRF-blocking HTTP client; audit payloads redact hold tokens and carry the listing's sub-tenant scope.

API surface

Endpoints are versioned under /trevize/v1/. Two surfaces share it: the partner API (/partner/…, authenticated by a Mannix ApiKey bearer token with scopes trevize:availability:read and trevize:bookings:write) and the tenant admin surface behind the console. Errors are RFC 7807 on both.

Trevize endpoints in the Foundation API reference OpenAPI 3.1 schema for Trevize with request/response shapes, parameters, and a try-it client.

Quick reference

MethodPathPurpose
GET/trevize/v1/partner/listingsActive listings with offer metadata: duration shape, party bounds, effective lead-time policy.
GET/trevize/v1/partner/listings/{id}/availability?from=&to=&party=Seldon-backed slots with listing overrides and allotment headroom applied. Window capped at 31 days.
POST/trevize/v1/partner/bookingsCreate + auto-confirm a booking. Idempotent on (channel, externalRef) — replays return the prior result.
POST/trevize/v1/partner/bookings/{externalRef}/cancelCancel; respects the offer's cancel deadline (409 past it, unless the channel allows late cancel).
PATCH/trevize/v1/partner/bookings/{externalRef}Modify partyCount or slotRef; reprice-guarded.
GET/trevize/v1/partner/bookings/{externalRef}Status read, including the Seldon status mirror.
POST / GET/trevize/v1/channelsCreate a Channel (mints the partner ApiKey — plaintext shown once) or list channels.
POST/trevize/v1/channels/{id}/webhookConfigure the availability-push webhook URL and signing secret.
POST / GET/trevize/v1/channels/{channelId}/listingsPublish Offers to a channel; set overrides, allotments, and price.
GET/trevize/v1/external-bookingsThe channel-bookings ledger: channel ref ↔ booking link, commission column.
GET / POST/trevize/v1/webhook-deliveries · /{id}/redeliverDelivery log with statuses and attempts; redeliver on demand.
POST / GET/trevize/v1/feed-tokens · /{id}/revokeMint, list, and revoke iCal feed tokens.
GET/trevize/v1/feeds/{token}.icsThe tokenized iCal export feed (rolling window of confirmed bookings).
GET/trevize/v1/reports/commissionBookings and owed commission by channel by ISO week.

Example: a partner reads court availability, then books

A local-sports aggregator lists a racquet club's court-hour offer. It reads near-term availability, then books a slot for two — echoing back the opaque slotRef it was given.

GET /trevize/v1/partner/listings/tlst_01JX2K…/availability
    ?from=2026-07-15T00:00:00Z&to=2026-07-16T00:00:00Z&party=2
Authorization: Bearer lk_…

→ 200 OK
[
  {
    "slotRef":        "eyJ2IjoxLCJrIjoiZ3JpZCIs…",
    "startsAt":       "2026-07-15T17:00:00Z",
    "endsAt":         "2026-07-15T18:00:00Z",
    "available":      3,
    "unitPriceCents": 4500,
    "currency":       "USD"
  },
  …
]
POST /trevize/v1/partner/bookings
Authorization: Bearer lk_…
{
  "listingId":   "tlst_01JX2K…",
  "slotRef":     "eyJ2IjoxLCJrIjoiZ3JpZCIs…",
  "partyCount":  2,
  "externalRef": "citysports-order-88412",
  "customer":    { "displayName": "Jordan Ellis", "email": "jordan@example.com" }
}

→ 201 Created
{
  "externalRef":         "citysports-order-88412",
  "status":              "confirmed",
  "seldonStatus":        "CONFIRMED",
  "partyCount":          2,
  "startsAt":            "2026-07-15T17:00:00Z",
  "endsAt":              "2026-07-15T18:00:00Z",
  "customerDisplayName": "Jordan Ellis",
  "priceCents":          4500,
  "currency":            "USD"
}

Replaying the same POST with the same externalRef returns 200 with the identical, single booking — at-least-once partners are safe by construction. Behind the scenes, the booking took real Seldon holds, the customer merged into Terminus by email, and a channel-settled Mallow invoice posted — and every other subscribed partner got a signed availability.changed webhook telling it to re-pull that window.

How it fits with the rest

flowchart TB
  P[Partner / aggregator] -- ApiKey auth --> Tv(Trevize partner API)
  Tv -- signed webhooks + ICS feeds --> P
  Tv -- verify key --> Mx[Mannix]
  Tv -- read availability, book --> Se[Seldon]
  Tv -- resolve customer --> Te[Terminus]
  Tv -- channel-settled invoice --> Ma[Mallow]
  Se -. booking.* events .-> Tv
            

Mannix mints and verifies the machine-partner ApiKeys — the tenant on every partner request comes from the verified key, nothing else. Seldon is the arbiter of supply: channel bookings take the same holds and respect the same access rules and cancel deadlines as the first-party booking widget and the Hober-driven console, so a channel can never double-sell a slot. Terminus merges channel customers into the tenant's one person graph. Mallow accounts for channel-collected money as a receivable from the channel. Demerzel hosts the /dashboard/channels console — channels, listings, the external-bookings ledger, webhook delivery log, and feed tokens. Availability changes fan out from the same audit outbox the rest of the platform reads, as signed webhooks to partners.