Skip to main content
The distribution module (@voyant-travel/distribution) owns the broader commercial network: the Channels that sell an operator’s inventory, the contracts and commission rules that govern them, the mappings and allotments that target inventory at each channel, the outbound channel push that keeps upstream systems in sync, and the reconciliation and settlement that close the financial loop. It also carries supplier-side identity and external references. It is cross-cutting channel support, not a separate user base. Distribution is the outbound complement to the catalog source-adapter contract. Where catalog’s inbound direction pulls projections, prices, and content from upstream sources, distribution pushes the inverse: when a booking commits, an availability changes, or content is edited on Voyant, distribution syndicates that change to the channels that resell the inventory. It extends bookings and inventory through the established distributionBookingExtension pattern, so those modules stay clean of channel concepts while distribution depends on them.

Key concepts

Channel
distribution counterparty
A counterparty that sells the operator’s inventory: direct, OTA, affiliate, reseller, marketplace, or API partner. A first-class entity (channels) with its own contracts, contacts, commission rules, allotments, and reconciliation surface. See Channel.
Channel Contract
agreed terms
The agreed commercial terms with a Channel (channel_contracts): commission, compensation policy, rate-limit overrides, and the like. See Channel Contract.
Commission Rule
scoped earning rule
A scoped (booking, product, rate, or category) fixed-or-percentage rule for what a Channel earns (channel_commission_rules). See Commission Rule.
Channel Product Mapping
product-level identity
The relationship between a Voyant product, option, or category and the channel’s external identifiers (channel_product_mappings). Used at booking-push time to translate a Voyant booking item into the upstream product reference. One row per channel and product.
Channel Inventory Allotment
per-slot targeting
Per-slot, per-option, per-start-time inventory targeting (channel_inventory_allotments): how many units of which option a channel sells for a given departure. Used at availability-push time to know which slots a channel cares about. Many rows per channel and product. Release rules return unsold allotment to general capacity.
The relationship between a Voyant booking and the channel’s booking or reference (channel_booking_links): external booking id, external status, last-synced timestamp, and the push status that tracks an in-flight or completed outbound push.
Channel push
outbound sync
The outbound direction of supplier integration: booking push (per-booking, transactional), availability push (per-slot-change, idempotent, eventually-consistent), and content push (per-edit, infrequent). Each shares the SourceAdapter contract but uses a distinct durable-intent and workflow code path because the retry and error semantics differ.
Reconciliation
expected vs actual
Comparison of expected versus actual bookings and amounts against a Channel (channel_reconciliation_runs, channel_reconciliation_items), producing Reconciliation Issues such as missing_booking, status_mismatch, amount_mismatch, cancel_mismatch, and missing_payout. It also catches divergence after a long channel outage. See Reconciliation.
Settlement
financial close
An accounting run reconciling booking-side amounts owed to or from a Channel (channel_settlement_runs, channel_settlement_items), with approval and policy surfaces. See Settlement.

What it owns

  • Channels and contracts: the channels and channel_contracts tables, channel contacts, and the per-channel commercial terms.
  • Commission rules: channel_commission_rules, scoped by booking, product, rate, or category.
  • Mappings and allotments: channel_product_mappings (product-level identity), channel_inventory_allotments and their targets (per-slot inventory), and the release rules and schedules that govern unsold allotment.
  • Channel push: the durable-intent tables (channel_availability_push_intents, channel_content_push_intents), the push-status fields on channel_booking_links and channel_product_mappings, and the per-flow workflows. Subscribers write durable intent rows and return immediately; durable workflows do the HTTP so the in-process event bus is never blocked.
  • Reconciliation and settlement: the run, item, policy, and approval tables for both, plus remittance-exception handling.
  • Inbound channel webhooks: channel_webhook_events logs events received from channels (the opposite direction from outbound push).
  • Suppliers and external refs: supplier identity, services, rates, and notes (./suppliers), and per-entity upstream identifiers (./external-refs).
  • Per-channel rate limiting: token-bucket config on channels and contracts so booking pushes pre-empt availability and content pushes while sharing one upstream budget.
It does not own bookings, products, or finance truth; it references them by id and extends them through the booking extension. The catalog plane stays neutral, owning the adapter contract while distribution owns the operational push state.

Working with it

Distribution registers as a module like any other.
import { distributionModule } from "@voyant-travel/distribution";
import { createApp } from "@voyant-travel/hono";

const app = createApp({
  modules: [distributionModule],
  // ...
});
Channel push is the operational heart. A booking commit triggers a write-only subscriber that records durable intent and returns; a durable workflow does the actual upstream calls with retries, rate limiting, and compensation.
// Subscriber: re-fetch state, write pending links, trigger the workflow. No HTTP here.
eventBus.subscribe("booking.confirmed", async ({ data }) => {
  const booking = await readBookingWithItems(db, data.bookingId);
  const channels = await resolveChannelsForBooking(db, booking); // via channel_product_mappings
  if (channels.length === 0) return; // owned product not syndicated

  for (const { itemId, channel } of channels) {
    if (!channel.mapping.pushBookings) continue;
    if (!channel.adapter.capabilities.supportsBookingPush) continue;
    await upsertChannelBookingLink(db, {
      bookingId: booking.id,
      bookingItemId: itemId,
      channelId: channel.id,
      sourceConnectionId: channel.connectionId,
      pushStatus: "pending",
      idempotencyKey: stableKey(booking.id, itemId, channel.id),
    });
  }

  await workflows.trigger("channel.booking.push", { bookingId: booking.id });
});
The outbound contract extends the same SourceAdapter used for inbound. One adapter instance per connection carries both directions; capability flags separate them.
export interface SourceAdapter {
  // ...inbound methods (discover, liveResolve, reserve, cancel) ...
  pushBooking?(ctx, request): Promise<PushBookingResult>;
  pushAvailability?(ctx, request): Promise<PushAvailabilityResult>;
  pushContent?(ctx, request): Promise<PushContentResult>;
}
Resolve channels with the right table for each flow, or you get a correctness bug. Booking push and content push use channel_product_mappings (product-level), because they translate a product reference and content is product-shaped. Availability push uses channel_inventory_allotments (per-slot), so it only pushes slots a channel actually has an allotment for. Resolving booking push via allotments misses channels sold “on request”; resolving availability push via mappings floods every channel with every slot.
For the marketplace direction (selling owned inventory through Viator or GetYourGuide), distribution is the home for the channel facade: those integrations are distribution channel adapters that sit on top of inventory, commerce, and bookings, never catalog source adapters. Voyant stays the operator’s system of record; the marketplace reads availability and price and sends reservations back through the adapter, which creates real local bookings and allocations.
  • bookings is extended by distributionBookingExtension and is the trigger for booking push; channel booking links cross-reference Voyant bookings to channel references.
  • inventory supplies the Slots that allotments target and the availability changes that drive availability push.
  • catalog owns the SourceAdapter contract distribution extends for outbound, and the registry keyed by connection.
  • commerce supplies channel and commission context for the commercial decision; what a channel earns is settled here.
  • finance consumes settlement output and remittance, and reconciliation issues feed financial dispute handling.

React package

The @voyant-travel/distribution-react family provides the staff-facing channel UI: channel setup, contract and commission management, product and option mapping, allotment configuration, the channel-sync health view backed by push status, and the reconciliation and settlement surfaces. Operator language stays explicit (“Sell this owned product on Viator”, “Channel booking”, “Channel reference”); rate-limit tokens and intent tables stay invisible.

Next steps

Catalog

The inbound source-adapter contract distribution extends for outbound push.

Inventory

The Slots and availability that allotments target and push to channels.

Commerce

Channels, commission, and the commercial decision that scopes a sale.

Glossary

Channel, Commission Rule, Settlement, and Reconciliation defined.