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.
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 akind(partner_apiorical), astatus(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, andcommissionBps— reporting only; Trevize computes owed commission but never moves money. - ChannelListing
- A Seldon Offer published to a Channel (
tlst_…), unique per(channel, offer).overridesnarrow only — display name, lead times, party max; a listing can never widen what the Offer allows.allotmentcaps (maxConcurrentBookings,maxPerDay) protect direct capacity from being swallowed by a channel.unitPriceCents+currencyare the price basis for channel invoices. - ExternalBooking
- The mapping row and the heart of idempotency (
txbk_…): channel, listing, the channel'sexternalRef, the realseldonBookingId, 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
WebhookDeliveryrows (exponential backoff, five attempts, thendead+ 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 Settlementasset 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: anical-kind Channel subscribes to an external ICS per resource — a Daneel-scheduledtrevize.ical_pulldiffs 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/commissionaggregates 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
PATCHa booking'spartyCountorslotRefwith 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
PartnerAuthenticatoris 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.
Quick reference
| Method | Path | Purpose |
|---|---|---|
| GET | /trevize/v1/partner/listings | Active 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/bookings | Create + auto-confirm a booking. Idempotent on (channel, externalRef) — replays return the prior result. |
| POST | /trevize/v1/partner/bookings/{externalRef}/cancel | Cancel; 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/channels | Create a Channel (mints the partner ApiKey — plaintext shown once) or list channels. |
| POST | /trevize/v1/channels/{id}/webhook | Configure the availability-push webhook URL and signing secret. |
| POST / GET | /trevize/v1/channels/{channelId}/listings | Publish Offers to a channel; set overrides, allotments, and price. |
| GET | /trevize/v1/external-bookings | The channel-bookings ledger: channel ref ↔ booking link, commission column. |
| GET / POST | /trevize/v1/webhook-deliveries · /{id}/redeliver | Delivery log with statuses and attempts; redeliver on demand. |
| POST / GET | /trevize/v1/feed-tokens · /{id}/revoke | Mint, list, and revoke iCal feed tokens. |
| GET | /trevize/v1/feeds/{token}.ics | The tokenized iCal export feed (rolling window of confirmed bookings). |
| GET | /trevize/v1/reports/commission | Bookings 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.