Skip to content

Promoters Module Integration Guide

Related documentation: Data ownership

KISUM PLATFORM — PROMOTERS MODULE INTEGRATION GUIDE

Section titled “KISUM PLATFORM — PROMOTERS MODULE INTEGRATION GUIDE”

Version: 1.0.0
Audience: junior frontend developers, junior backend developers, QA, partners
Status: integration guide


This document explains how to call the Base Backend correctly in the new architecture.

It covers:

  • how auth works from the caller point of view
  • how to send Authorization
  • how to send x-org
  • what sequence to follow
  • how to think about errors
  • working request examples
  • what not to do

This is the “how to integrate” document.

For full architecture, see:

For the API catalog, see:


You do not log in through Base Backend.

You:

  1. authenticate through Auth Backend
  2. obtain a JWT
  3. choose the active company
  4. call Base Backend with:
    • Authorization: Bearer <JWT>
    • x-org: <COMPANY_ID>

Base Backend then:

  • validates the JWT
  • validates x-org
  • resolves effective access from Auth
  • checks promoter entitlement (access.modules includes any of promoter, promoters, basic_promoter, or basic_promoters; legacy object shape may still expose basic)
  • checks permission
  • allows or denies the route

Every protected request must include:

Authorization: Bearer <JWT_FROM_AUTH_SERVICE>
x-org: <COMPANY_ID>
Content-Type: application/json

x-org is the only supported active-company input for tenant-scoped Base requests. Do not use query parameters or implicit company fallback for runtime access.

Must be a token issued by Auth.

Must be the active company ID for the request.

Usually application/json, unless the route uses uploads.


The user signs in through Auth, not Base.

Result:

  • JWT token
  • authenticated user session

Caller can use Auth-side identity endpoints as needed.

Frontend-Kisum-Promoters does not auto-select a company on login when multiple Promoters-eligible tenants exist. After Auth returns a JWT:

  1. The app loads /dashboard (normal shell). Branch on promoterEligibleCompanies from the unscoped GET /users/init:

    • 0 eligibleSubscribe overlay (main pane): pick a membership without Promoters → cookie → /profile/billing. Companies that already have Promoters are excluded.
    • 1 eligible → auto-set cookie, scoped init, full app (picker fallback if auto-select fails).
    • 2+ eligiblePromoters overlay (main pane): pick entitled company only → scoped init.
  2. Billing gate: if the active company lacks Promoters entitlement, non-allowlist routes redirect to /profile/billing. Allowlist: /profile, /profile/companies (+ detail), /profile/billing. Sidebar product nav stays visible but greyed (“Subscribe to unlock”). Profile menu lists all memberships for switching; header switcher lists Promoters-eligible only.

  3. Self-serve company delete (no package): on /profile/companies, TENANT_SUPERADMIN may delete an empty company shell via GET|DELETE /api/users/companies/:id (+ delete-preview). No x-org. See promoters API §Profile companies.

  4. Where the eligible list comes from (changed 2026-07-27): BFF matches Auth package, Core basePackage (e.g. promoters, basic_promoters), and module entitlements — not only exact promoter. It is served from two places:

    • Unscoped GET /users/init (no x-org) — still embeds promoterEligibleCompanies, because that is the “no company chosen yet” bootstrap that renders the picker.
    • GET /users/companies/eligible — the lazy endpoint for everything else (switcher dropdown).

    A scoped init (x-org set) deliberately omits promoterEligibleCompanies. Resolving it costs one Auth access + one Core snapshot per company, so embedding it in every init made login time scale with membership count. Clients must treat an absent list as “not loaded”, never as “empty” — overwriting a loaded list with [] empties the switcher.

That value is then sent on every tenant-scoped BFF call as:

x-org: <COMPANY_ID>

Send the business request with:

  • JWT
  • x-org

Base validates:

  • JWT
  • x-org
  • effective access from Auth
  • promoter module (or package aliases: promoters, basic_promoter, basic_promoters)
  • permission

Since 2026-08-06, legacy /api/countries* and /api/regions* also read Artists Postgres geo (machine BFF via services/artistsGeo.client.js). Mongo country/region collections are no longer used on those paths.

Since 2026-05-27, Promoters exposes /api/artists-network/* as a browser-facing BFF to Backend-Kisum-Artists /api/v1/*.

Since 2026-06-07, legacy GET /api/artists, GET /api/artists/search, and GET /api/artists/:id also proxy to the same Artists directory API (machine BFF, legacy response shapes). Mongo artist master reads are deprecated for those routes.

Since 2026-07-10, the directory BFF mapper passes through slug from Artists Postgres (artists.slug) on list/search/detail rows so Frontend-Kisum-Promoters can deep-link /artists/{slug} (fallback: numeric id).

Since 2026-07-27, Artists GET /api/v1/artists?q= ranks by name match quality (exact → prefix → word → contains → similarity) before chart rank. Promoters GET /api/artists and GET /api/artists/search inherit this automatically — no BFF change required.

Since 2026-07-28, the ranked query uses Artists’ trigger-maintained normalized_name column directly, allowing its GIN trigram index to serve uncached names. Promoters remains a thin BFF; an x-cache: MISS must not trigger a full Artists table scan.

Callers still use Promoters as the primary origin (:3099) with the same headers:

Authorization: Bearer <JWT_FROM_AUTH_SERVICE>
x-org: <COMPANY_ID>

Promoters validates access, then forwards Authorization + x-org upstream. Example:

Terminal window
curl -X GET 'http://localhost:3099/api/artists-network/marketplace/artist-availabilities?from=2026-06-01' \
-H 'Authorization: Bearer <JWT>' \
-H 'x-org: <COMPANY_ID>'

Maps to Artists GET /api/v1/marketplace/artist-availabilities (unified official + middle-agent listings; each row has official: true|false).

New booking flows should use /api/artists-network/booking-requests, /booking-offers, /bookings — not legacy Mongo /api/offer or /api/avails.

Full path map: Backend-Kisum-Promoters/docs/ARTISTS_NETWORK_BFF.md.

  • Source of truth: Backend-Kisum-Artists artist_profiles via GET|PUT /api/v1/artists/{id}/profile on the Artists service (persona operators) or platform Adminnot the Promoters BFF.
  • Promoters BFF: GET /api/artists-network/artists/{id}/profile only (read-only proxy). Removed from Promoters (2026-07-14): PUT profile, POST .../ensure-short-bio, POST .../ensure-bio-formatted, legacy POST /api/artists/:id/bio.
  • Promoters UI: artist directory and biography pages are view-only; no background bio generation or profile mutation.

Frontend (Frontend-Kisum-Promoters): marketplace avails and booking pipeline use artists-network BFF. Login uses NEXT_PUBLIC_AUTH_BASE_URL, not Promoters /api/auth/*.

Unified marketplace offer enquiry (2026-07-14)

Section titled “Unified marketplace offer enquiry (2026-07-14)”

Exclusive (official) and secondary (middle-agent listing) shelves both open the same structured offersheet at /booking/marketplace/enquire?listingId=<uuid> (ArtistOfferEnquiryPage). The browser passes listing preview meta in session storage; submit uses the neutral POST /api/artists-network/marketplace/offers route with availabilityListingId and an explicit offerChannel (exclusive or middle_agent). Bulk offers keep this value per target.

Promoters forwards that value to Artists POST /api/v1/marketplace/offers. Artists resolves the actual listing and rejects a mismatched channel. Official offers return the official agency disclaimer and persist in normal booking_offers; middle-agent offers return the secondary-market disclaimer and persist in middle_agent_offers. Legacy middle-agent/enquiries paths are Middle Agent only, not Exclusive aliases.

After submit, and from the combined Offers inbox, marketplace records link to /booking/offers/:id?source=marketplace. Exclusive rows now share the normal booking table, while Middle Agent rows remain separate; the marker still selects the neutral marketplace representation and preserves the correct offersheet/detail copy. The returned channel controls the badge, seller label, and notice: official records show Exclusive / Official agency; only secondary records show Middle agent.

Exclusive seller ownership is the approved Artists agency claim linked to its Core x-org; it never requires a Middle Agent operator. The agency accepts/declines, the promoter cancels, and counters preserve the Exclusive channel. An accepted Exclusive offer creates the normal booking used by contracts, touring, and logistics. A Middle Agent acceptance remains inside the broker workflow and does not create artist touring/logistics access.

Ownership: Backend-Kisum-Promoters Postgres (promoters_db, Prisma model offline_offers). Not proxied to Artists; no orchestrateMiddleAgentEnquiryBody or forced event creation on save.

MethodPathPermission
POST/api/booking/offline-offerspromoter.booking.offers.create
GET/api/booking/offline-offerspromoter.booking.offers.view
GET/api/booking/offline-offers/:idpromoter.booking.offers.view
PATCH/api/booking/offline-offers/:idpromoter.booking.offers.create
DELETE/api/booking/offline-offers/:idpromoter.booking.offers.create
  • Tenant scope: canonical Core company UUID from x-org (coreCompanyId column).
  • artistId is required (numeric Artists directory id). Optional availabilityListingId is stored as a reference only.
  • Status defaults to offline. Full wizard payload is stored in offersheet JSON.
  • Env: DATABASE_URLpromoters_db; run npx prisma migrate deploy after deploy.
  • Browser: bare /booking/marketplace/enquire = offline; ?listingId= = marketplace (unchanged).

When a show chooses create event on submit, the wizard selects the renameable Core BU identified by metadata.systemTemplateKey = talent-programming by default, because the event starts from an artist offer. Users may add other BUs, and the offersheet row must retain at least one active Core businessUnitIds UUID. Promoters requires promoter.event.create and evaluates its V2 BU scope before creating the event. A show linked to an existing promoter event inherits that event’s scope. GET /api/access/business-units is therefore available to authenticated tenant members for operational pickers; BU mutations remain access-management-only.

The same create-on-submit path stores shows[].venueId as the canonical Backend-Kisum-Venues UUID and validates that directory record, matching normal Event creation. This is only an Event venue reference: it does not require a promoter-owned venue and does not create a Venue marketplace booking.

Listing id resolution (Artists):

ShelfBrowse sourcelistingId at enquire
Exclusiveartist_availabilities (official: true)availability window UUID
Secondarymiddle_agent_availability_listings (official: false)published listing UUID

GET /api/v1/marketplace/middle-agent-availability-listings/:id (and matching allowed-territories reads) accept either id shape. Official rows return representation_label: exclusive and the agency disclaimer; canonical offer metadata includes artistAvailabilityId, officialListing: true, and offerChannel: exclusive.

Official detail parity (2026-07-15): Exclusive listing detail resolves the same artist_availabilities UUID as browse and calendar. Seller agency name is enriched when representation/company claims match, but missing claim linkage no longer returns 404 for a row that already appears on the Exclusive shelf. Promoters BFF passes through upstream 404 bodies without applying middle-agent disclaimer defaults.

Listing details + calendar (Promoters UX):

  • Browser route (canonical): /booking/marketplace/listings/:listingId — full page with listing summary, month heatmap calendar, and Make offer. Legacy /booking/marketplace/artists/:artistId/listings/:listingId redirects here (artist UUID must not appear in public URLs).
  • Calendar selection → offer: promoters may select available (green) and on hold (amber) days only; confirmed (gray) and blocked are not selectable. Click one day, drag for an adjacent range, or Alt+click for non-adjacent multi-select. Sidebar Make an offer opens the enquiry wizard with one locked show row per selected date (showDates query + session preview meta). Top Make an offer (no calendar pick) keeps the existing flexible offer flow. Event link/create per show is unchanged.
  • BFF (promoter browse permission — not middle-agent path): GET /api/artists-network/marketplace/artists/:artistId/listings/:listingId (+ /calendar, /allowed-territories) → Artists GET /api/v1/marketplace/middle-agent-availability-listings/:id (upstream resolver accepts official artist_availabilities UUID or middle-agent listing UUID). Response includes official / representation_label so the UI branches correctly.
  • Legacy BFF /api/artists-network/middle-agent/availability-listings/:id remains for backwards compatibility; new Promoters UI uses /marketplace/... only.

Since 2026-05-27, Promoter events expose a stable UUID for cross-persona references:

FieldStoredAPI alias
publicEventIdMongo events.publicEventId
promoterEventId(same value)Venue JSON contract
  • Set on create (POST /api/events, POST /api/v2/events).
  • Returned on event GET/list/PATCH responses (additive — Mongo _id unchanged).
  • Legacy rows without the field receive a UUID on first GET-by-id or internal resolve.

Venue linkage: persist promoterEventId = Promoter publicEventId (UUID), not Mongo _id.

Event venue field (2026-06-30): Mongo events.venue stores the canonical Backend-Kisum-Venues Postgres UUID (public.venues.id). Create/update validates via GET /internal/admin/venues/:id (venue-directory.client.js). API responses hydrate venue as { venueId, name, cityName?, countryIso2? }not legacy Mongo venues._id. Browser event form searches GET /api/venues/marketplace/venues (sleeping + active). Legacy GET /api/venues Mongo search is not used for new event flows.

Machine resolver (for Venue Phase 3 client):

GET /internal/events/:eventId
X-Internal-API-Key: <PROMOTERS INTERNAL_API_KEY>

:eventId accepts publicEventId (preferred) or Mongo _id. Returns minimal event payload or 404.

Full contract: Backend-Kisum-Promoters/docs/PROMOTER_EVENT_ID.md.

Event booking tab + offer/request linkage (2026-06-29, updated 2026-06-30)

Section titled “Event booking tab + offer/request linkage (2026-06-29, updated 2026-06-30)”
RoutePurpose
GET /api/events/:id/booking-marketplaceLineup artists + filtered middle-agent listings and exclusive avails per artistsDirectoryId (Postgres artists.id)
GET /api/events/:id/lineupReturns lineup artists with id = Postgres artists.id; current writes store that numeric id directly (only older Event records may still contain legacy ObjectId refs until re-saved)
GET /api/events/linkable-for-artist-booking?artistIds=Comma-separated Postgres artists.id values (legacy artistsUuid param deprecated). Loads upcoming pending/confirmed company events, resolves each lineup via Artists upstream, then filters by canonical artists.id. Each row includes artistName from the event lineup (matched artist) for offer dropdown labels: EventName - ArtistName - EventDate. Also returns a location snapshot for offer prefill: countryId, countryName, countryIso2, cityId, cityName, venueId, venueName, venueCompanyId, capacity (resolved from stored event geo ids, venue profile, and Artists geo when legacy text-only city/country).
Offer wizard → BFFbooking/event-link-orchestrator.js validates/creates events + tours before Artists enquiry POST/PATCH (required for offers)
POST /api/v2/events lineupEach row artist = Postgres artists.id (positive integer). UUID and Mongo ObjectId are rejected; the numeric id is validated through Artists and stored directly.
POST /api/artists-network/booking-requestsOptional promoterEventId — standalone requests do not need an event link

Frontend: /events/:mongoId/booking — one section per lineup artist; Make offer locks row 1 to the event’s publicEventId (required); Send request may pass the same id when available but works without it. Available for pending and confirmed events (not cancelled).

Offer wizard show row (2026-06-30): Promoter event picker is first on each show row. When an event is linked (selected or locked from booking tab), country/city/venue/capacity are prefilled from the event and read-only. No event for this artist leaves location fields editable.

Offer-created Event identity (2026-07-15): offersheet.targets[].officialArtistId is the numeric Artists PostgreSQL artists.id. Before Event creation, Promoters validates it through Backend-Kisum-Artists using the active x-org, then writes the numeric id to events.lineup. This flow never queries the retired Promoters Mongo artist model and never casts an Artist id to ObjectId.

Event create/edit city (2026-06-30): Promoter events persist optional city_id and country_id (Artists Postgres geo). Frontend event form uses GET /api/artists-network/geo/countries/:id/cities city picker (not free text); legacy events without ids resolve on edit load when possible.

Event city picker in Sheet (2026-07-31): ComboboxGeoCity must use Radix <Popover modal> (same as ComboboxCountries / ComboboxVenue). A non-modal popover inside the Add/Edit Event Sheet cannot be used reliably — nested dismissable-layer / pointer-events conflict with the sheet. Search typing filters the geo list even when free-text city names are disallowed (allowCustomName={false}); the form still commits only a selected Artists geo city id.

Offersheet fields: eventLinkMode, promoterEventIds[], per-show promoterEventId or createEventOnSubmit + proposedEventDate. Tour mode when 2+ show rows.


Since 2026-05-27, Promoters exposes /api/venues/* as a browser-facing BFF to Backend-Kisum-Venues.

Callers use Promoters (:3099) with promoter JWT + x-org. Marketplace browse and profile (GET /api/venues/marketplace/venues*) use Venues admin reads and do not require venue operator org. For spaces, availability, and booking, pass venueCompanyId (query/body) or x-venue-org — the BFF forwards that as upstream x-org (typically resolved on the venue detail page from admin profile company_id).

Dual venue identifiers (operator-scoped calls)

Section titled “Dual venue identifiers (operator-scoped calls)”

Venues rows expose two ids:

FieldExampleUse
venue_id (catalog)104736Marketplace URLs, admin browse/profile path param
id (Postgres UUID)39b58396-…venueId on spaces, bookings, and tenant reads

Rule: Promoters marketplace detail loads profile by catalog id, then resolves tenantVenueId = admin profile id (UUID) for:

  • GET /api/venues/booking-slots?venueCompanyId=&spaceId= — BFF forwards visibility=public so only marketplace-safe packages are returned embedded on each slot (no flat slot price_cents after Venue migration 000031).
  • GET /api/venues/bookings/:id/activity?venueCompanyId= — promoter-visible booking timeline (status changes + outbound venue communications); requires promoter.venue.booking.view and parent booking scope preflight (mirrors artists-network booking activity BFF).
  • POST /api/venues/bookings body venueId, bookingDate, slotIds, slotPackageSelections ([{ slotId, packageId }], one public package per slot; required when slotIds present)

Do not pass the URL catalog numeric id as venueId on operator-scoped queries — venue_spaces.venue_id FK references venues.id (UUID), not venues.venue_id.

Availability UX (Promoters): one calendar per selected space (read-only); single-space venues auto-select; multi-space venues require a space selector before loading blocks.

Example marketplace profile by venue id:

Terminal window
curl -X GET 'http://localhost:3099/api/venues/marketplace/venues/12345' \
-H 'Authorization: Bearer <JWT>' \
-H 'x-org: <PROMOTER_COMPANY_ID>'

Example marketplace browse:

Terminal window
curl -X GET 'http://localhost:3099/api/venues/marketplace/venues/sleeping?q=london' \
-H 'Authorization: Bearer <JWT>' \
-H 'x-org: <PROMOTER_COMPANY_ID>'

Example slot-based booking create (marketplace):

Terminal window
curl -X POST 'http://localhost:3099/api/venues/bookings' \
-H 'Authorization: Bearer <JWT>' \
-H 'x-org: <PROMOTER_COMPANY_ID>' \
-H 'Content-Type: application/json' \
-d '{
"venueCompanyId": "<VENUE_OPERATOR_COMPANY_ID>",
"venueId": "<VENUE_UUID>",
"spaceIds": ["space-1"],
"title": "Show night",
"bookingDate": "2026-07-07",
"slotIds": ["slot-1"],
"slotPackageSelections": [{ "slotId": "slot-1", "packageId": "pkg-standard" }],
"status": "inquiry",
"promoterEventId": "<PUBLIC_EVENT_ID_UUID>"
}'

Legacy free-window example (still supported when slots omitted):

Terminal window
curl -X POST 'http://localhost:3099/api/venues/bookings' \
-H 'Authorization: Bearer <JWT>' \
-H 'x-org: <PROMOTER_COMPANY_ID>' \
-H 'Content-Type: application/json' \
-d '{
"venueCompanyId": "<VENUE_OPERATOR_COMPANY_ID>",
"venueId": "<VENUE_ID>",
"spaceIds": ["space-1"],
"title": "Show night",
"startAt": "2026-06-01T18:00:00.000Z",
"endAt": "2026-06-01T23:00:00.000Z",
"status": "inquiry",
"promoterEventId": "<PUBLIC_EVENT_ID_UUID>"
}'

marketCompanyId is auto-filled from caller x-org when omitted.

Full path map: Backend-Kisum-Promoters/docs/VENUES_NETWORK_BFF.md.

Legacy: Mongo /api/venues master CRUD frozen for new flows — use Venues BFF.

Required Promoters env: VENUE_INTERNAL_BASE_URL + VENUE_INTERNAL_API_KEY (plus Artists env from §4.1).


Since 2026-05-27, writes to local Mongo market routes return HTTP 410 Gone with error.code: legacy_market_write_removed:

Legacy write prefixUse instead
/api/avails/* (POST/PUT/PATCH/DELETE)/api/artists-network/marketplace/artist-availabilities (browse); artist publishes in Artists
/api/offer/*/api/artists-network/booking-requests, /api/artists-network/booking-offers/*
/api/offer-agreement-templates/*Artists contracts workflow (when UI lands)
/api/agencies/* (writes)/api/artists-network/marketplace/agencies (browse); roster/team reads still legacy GET until sunset

GET/HEAD on those legacy prefixes still work for compat reads until Sunset: Sat, 01 Nov 2026 00:00:00 GMT (response includes Deprecation: true).

Full audit: Backend-Kisum-Promoters/docs/LEGACY_MARKET_ROUTES.md.

Frontend (Frontend-Kisum-Promoters): offer accept/reject/sent/detail/update/delete, permissions, active package, and agency list browse migrated; avails CRUD, agency roster/team writes, contract templates, and subscription paths remain to migrate. Dashboard shows Booking network panel from artists-network promoter dashboard; Venues → Marketplace tab uses venues-network BFF.


Since 2026-05-27, Promoter CRM domains stay in Backend-Kisum-Promoters — events, vendors, dashboard aggregates, tasks, approvals, workspace, and Finance orchestration (not source of truth).

Stays in PromotersDoes not duplicate in Mongo
/api/events, /api/festivals, /api/event-*, /api/dashboard/*Artist booking pipeline → /api/artists-network/*
/api/vendors, /api/tasks, /api/approval, /api/notificationsVenue master + bookings → /api/venues/*
/api/integrations/finance/* (proxy)Subscriptions/packages → Core

Dashboard tenant scoping (2026-07-20): GET /api/dashboard/events-overview, /upcoming-events, and /financial resolve the active company from x-org (Core UUID) and map it to the private Promoters Mongo workspace via companies.coreCompanyId. If that bridge does not exist yet (typical for a brand-new company), the API returns empty/zero metrics — it must never run an unscoped Mongo query across all tenants. Vendor counts on the overview are limited to vendors on that tenant’s scoped events.

Full manifest: Backend-Kisum-Promoters/docs/PROMOTER_CRM_OWNERSHIP.md.

Event overview financial chart (2026-07-15): the authenticated /events/[id] overview tab loads expense rows from Promoters Mongo (GET /api/event/finance/estimate/expense/:companyId/events/:eventId/invoices, with Finance invoice list as the actual fallback). It does not require the legacy Profile → Integrations Kisum bridge. Finance GET /api/kisum/{companyId}/events/{eventId} still serves approved invoice metadata from native Prisma when Kisum is disconnected; legacy Kisum event fetch runs only when a connection exists.

Optional BFF extensions (Phase 5): relationships/trust and touring/logistics reads under /api/artists-network/relationships/* and /api/artists-network/bookings/:bookingId/logistics*.


4.6 In-product self-serve billing (FE-Promoter-Phase A.1)

Section titled “4.6 In-product self-serve billing (FE-Promoter-Phase A.1)”

Since 2026-05-27, Frontend-Kisum-Promoters ships a self-serve billing surface at /profile/billing that lets a Promoter operator inspect their commercial state and manage their payment method without leaving the product app.

The surface composes three sources of truth — none of which is the product app itself:

SurfaceEndpointOwnerPurpose
Current plan + active add-onsGET /api/companies/active-packagePromoters BFF → Core subscription-summaryRead-only commercial state for x-org
Available add-ons catalogGET /api/catalog/addons?audience=promoter (Next.js proxy → Core /public/addons)Promoters FE → CorePublic catalog, no auth; same-origin avoids Core CORS on local app origins
Add / replace payment method${CHECKOUT}/billing/add-card?companyId=…&returnTo=…System-Kisum-CheckoutCard capture only happens inside Checkout
Add add-on${CHECKOUT}/billing/upgrade?companyId=…&addonKey=…&returnTo=…System-Kisum-Checkout → Core POST /internal/companies/{id}/addonsFree add-ons provision immediately; paid add-ons use Xendit + finalize
Cancel add-onPOST ${CHECKOUT}/api/billing/upgrade/cancel { companyId, addonKey }System-Kisum-Checkout → CoreCross-origin from product app with shared .kisum.io cookie

Hard rules preserved:

  • The Promoter app never mounts xendit-components-web. All paid flows deep-link to System-Kisum-Checkout.
  • The Promoter cannot switch their base persona package from this surface — that would orphan tenant data. Persona changes (e.g. Promoter → Venue) require platform-staff intervention in Frontend-Kisum-Admin.
  • The Promoter can add or cancel add-ons via Checkout (Option A, shipped 2026-06-09):
    • Browse → cart → checkout: /profile/billing uses an in-app add-on cart (session storage per company). Add to cart collects selections; Proceed to Checkout deep-links to ${CHECKOUT}/billing/upgrade?addonKeys=ai,finance&….
    • Add: Checkout validates membership + catalog, provisions $0 bundles immediately or opens Xendit when total > 0; finalize syncs Core + recurring plan.
    • Cancel: POST /api/billing/upgrade/cancel with { companyId, addonKey }.

Dev-only mock in-drawer checkout (2026-06-26)

Section titled “Dev-only mock in-drawer checkout (2026-06-26)”

When System-Kisum-Checkout is unavailable locally (missing NEXT_PUBLIC_CHECKOUT_BASE_URL or broken Xendit stack), persona apps Artists, Promoters, and Venues support an optional in-drawer mock checkout on /profile/billing:

FlagWherePurpose
NEXT_PUBLIC_MOCK_BILLING_CHECKOUT=truePersona frontendProceed to Checkout stays in the cart drawer; shows a fake card form
MOCK_BILLING_CHECKOUT_ENABLED=truePersona BFF (Artists/Promoters API or Venues Next server)Enables POST /api/billing/mock-checkout/complete
MOCK_BILLING_CHECKOUT_AUDIENCEBFF (optional)Catalog audience: artist / promoter / venue (defaults per persona)

Test cards (client-only):

  • 4242 4242 4242 4242 → success path → BFF provisions add-ons in Core (source: mock.checkout)
  • 4000 0000 0000 0002 → simulated decline (no Core write)

Hard rules:

  • Mock checkout must not ship enabled in production — backend returns 404 when the server flag is off.
  • Payment UI is fake; entitlements are real (Core POST /internal/companies/{id}/addons). No Xendit charge or recurring-plan sync.

The surface is a full self-serve read + mutate experience for add-ons (base package remains read-only).


4.6b Canonical company identity (2026-07-14)

Section titled “4.6b Canonical company identity (2026-07-14)”

The Core company UUID is the only company identity shared across services. Core has no Mongo alias and Finance has no company link/unlink APIs. Promoters may resolve old domain records through its private companies.coreCompanyId pointer, but that storage detail never appears in browser contracts, Core, Finance, or rollout commands.

4.7 Ticket sales lifecycle → Finance (2026-07-13)

Section titled “4.7 Ticket sales lifecycle → Finance (2026-07-13)”

Finance is the source of truth for REAL income, including ticket sales. The full lifecycle:

  1. Sales enter Promoters three ways: manual add-sales UI, CSV import, or the external Ticket Company Sales API (per-vendor x-api-key, day-set idempotent, rows tagged source: 'api'; the manual form warns when a ticket already receives API sales).
  2. Sales reach Finance via the daily cron push (/cron/finance-income-daily-push, CRON_SECRET; needs an external scheduler) or on demand: POST /api/integrations/finance/events/{eventId}/sync-income (tenant session, sales/commercial edit permission) pushes the event’s FULL history and returns per-status counts — surfaced as the “Sync to Finance” dropdown action on the event Tickets tab and the actual-income page.
  3. In Finance the ticket income is LIVE (workflowStatus DRAFT): it accrues one term per (event, vendor, ticket category, day, currency) and already counts in reports/profitability. The category joined the key on 2026-08-01 — vendor commission is charged per tier, so a vendor-day total could never express it, and a 2,000,000 VIP sale looked identical to four 500,000 GA sales. Ticket-company advance payouts are recorded as income payments (allowed while LIVE; a lump payout FIFO-fills the oldest days) — income stays gross, the payout only reduces what the ticket company still owes.
  4. After the event the promoter presses “Settle income” in Finance: the amount locks, the final invoice goes to Xero (if connected), and any further sync returns SETTLED_SKIPPED — settled income is never mutated by automation. Remaining payouts can still be recorded after settlement.

4.7a Ticket sales data model — the rules that must not drift (2026-08-01)

Section titled “4.7a Ticket sales data model — the rules that must not drift (2026-08-01)”

Design of record: Backend-Kisum-Promoters/docs/TICKET_SALES_ARCHITECTURE.md.

  • Sale amounts are stored, never recomputed from the current tier price. The model this replaced derived every historical figure from today’s price, so changing a price silently restated all history — and on the write side it could drive a receivable negative.
  • Migrated history is kept but not trusted as money. The 20,367 rows from the retired Mongo model carry value_source = ESTIMATED because the source never stored a price. They count as inventory evidence and serve as development data; their amounts never reach Finance.
  • ticket_order_lines.line_type is PAID | COMP (renamed from SALE). A comp is a sale of inventory — what separates the two is whether money was charged. Comps carry comp_reason and issued_by, consume comp_capacity only (already carved out of total_capacity), and may still carry charges: a free ticket can have a booking fee.
  • Orders and daily aggregates are never both authoritative for the same scope and period. ticket_sales_source_authority decides, most-specific-wins (category > vendor > channel > currency). Two rules of equal specificity covering the same period are refused (409), not tie-broken — resolving the tie would hide the author’s mistake behind a figure that still looks correct.
  • Settlement belongs exclusively to Finance. Promoters caches a settlement reference and never holds financial truth. See Finance API → Ticket money.
  • Six charge types, three accounting treatments. Tax is a liability to remit; the buyer fee is money the customer paid on top that the promoter never receives; the promoter fee is the promoter’s own revenue. Only commission, processing fee and withholding reduce the receivable. The feed sends all of them separately — the blended fees field it used to send subtracted tax and the buyer fee too, understating what every vendor owed.
  • Vendor report semantics live in ticket_vendor_mapping_profiles, not in code. “Gross sales” is face value for one ticket company and face value plus tax plus booking fee for the next. The profile records the reading (timezone, what is bundled into gross, whether commission is pre-deducted, whether refunds arrive as negative rows) with effective dates, so a file imported in March is still explained by the profile live in March. An unknown mapping_version is refused, never silently swapped.
Terminal window
curl -X GET 'http://localhost:3099/api/events?type=all&page=1&limit=20' -H 'Authorization: Bearer <JWT>' -H 'x-org: <COMPANY_ID>'
  • token valid
  • company exists in user access scope
  • promoter module enabled
  • user has event view permission
  • success → 200
  • bad token → 401
  • bad company → 400 or 403
  • no access → 403
  • access resolution unavailable → 503

Terminal window
curl -X GET 'http://localhost:3099/api/artists' -H 'Authorization: Bearer <JWT>' -H 'x-org: <COMPANY_ID>'
Terminal window
curl -X POST 'http://localhost:3099/api/agencies' -H 'Authorization: Bearer <JWT>' -H 'x-org: <COMPANY_ID>' -H 'Content-Type: application/json' -d '{
"key": "agency-001",
"name": "Example Agency"
}'
Terminal window
curl -X PUT 'http://localhost:3099/api/agencies/{id}' -H 'Authorization: Bearer <JWT>' -H 'x-org: <COMPANY_ID>' -H 'Content-Type: application/json' -d '{
"name": "Updated Agency Name"
}'
Terminal window
curl -X DELETE 'http://localhost:3099/api/agencies/{id}' -H 'Authorization: Bearer <JWT>' -H 'x-org: <COMPANY_ID>'
Terminal window
curl -X POST 'http://localhost:3099/api/files/upload' -H 'Authorization: Bearer <JWT>' -H 'x-org: <COMPANY_ID>' -F 'file=@example.pdf'

Base is not the login service.

If the route is tenant-scoped, x-org is required.

The UI or calling layer must know the active company and send it explicitly.

Just because a button is visible or hidden does not mean the backend will allow the route.

Do not use:

  • Auth
  • Users
  • Company Users
  • Invitations
  • Teams
  • Packages
  • Permissions
  • Role
  • Subscription

Those moved to Auth/Core.


Since 2026-05-27, Backend-Kisum-Promoters returns HTTP 410 Gone (not 404) for route trees that moved to Auth or Core:

  • /api/auth/*Auth
  • /api/subscription/*, /api/packages/*Core
  • /api/role/*, /api/permissions/*Auth (use GET /api/users/permissions on Promoters for compat reads)
  • POST|PUT|PATCH|DELETE /api/companiesCore (Promoters keeps GET-only company BFF reads)

Response shape:

{
"success": false,
"error": {
"code": "route_removed",
"message": "This route was removed from Backend-Kisum-Promoters.",
"owner": "Backend-Kisum-Auth",
"hint": "Authenticate via AUTH_BASE_URL (login, refresh, password flows).",
"path": "/api/auth/login",
"documentation": "docs/DEAD_ROUTES.md"
}
}

Do not retry these against Promoters — migrate the caller to the owning service.

8.0.1 410 Gone — legacy market writes (Phase 4)

Section titled “8.0.1 410 Gone — legacy market writes (Phase 4)”

Since 2026-05-27, POST/PUT/PATCH/DELETE on Mongo market routes return 410 with error.code: legacy_market_write_removed:

  • /api/avails/*
  • /api/offer/*
  • /api/offer-agreement-templates/*
  • /api/agencies/*

Use /api/artists-network/* for new booking and marketplace flows. GET compat reads remain until 2026-11-01. See §4.4 and docs/LEGACY_MARKET_ROUTES.md.

Usually means:

  • missing x-org
  • malformed x-org
  • bad request payload

Usually means:

  • missing token
  • invalid token
  • expired token
  • token rejected during validation

Usually means:

  • membership not valid for x-org
  • promoter module missing
  • permission missing

Usually means:

  • Base could not resolve effective access from Auth
  • Auth service unavailable
  • network failure / timeout between services

503 must not become temporary access.
The request must fail closed.


Do not do any of the following:

  • call /auth/* on Base
  • use Base as the source of identity
  • use Base as the source of package/subscription truth
  • infer access from the JWT alone
  • assume company from frontend state without sending x-org
  • silently retry with a different company
  • bypass permission checks because a user is “probably admin”
  • call /auth/me/access using ?companyId=… instead of x-org

A typical frontend flow should look like this:

  1. User signs in with Auth
  2. Frontend stores current JWT securely
  3. Frontend gets or already knows active company
  4. Frontend calls Base with JWT + x-org
  5. If route denied:
    • show proper error
    • do not guess fallback
  6. If 503:
    • show service unavailable / retry flow
    • do not fake access

If another internal backend calls Base on behalf of a user:

  1. obtain or forward a valid Auth-issued JWT
  2. send explicit x-org
  3. call Base route
  4. respect 401 / 403 / 503
  5. do not add local bypass logic

Examples of route-level access expectations:

  • GET /dashboardpromoter.dashboard.view
  • GET /artistspromoter.artist.view
  • POST /artistspromoter.artist.create
  • PUT /artists/{id}promoter.artist.edit
  • DELETE /artists/{id}promoter.artist.delete
  • GET /eventspromoter.event.view
  • POST /eventspromoter.event.create
  • PUT /events/{id}promoter.event.edit
  • DELETE /events/{id}promoter.event.delete
  • GET /vendorspromoter.vendor.view
  • POST /vendorspromoter.vendor.create
  • GET /venuespromoter.venue.view

These examples are helpful for integration teams even if the exact permission map is maintained elsewhere.


13. Active route families commonly used by integrations

Section titled “13. Active route families commonly used by integrations”

Integrations will most often use:

  • Agencies
  • Agreements
  • Ai
  • Approval
  • Artists
  • Avails
  • Companies (business metadata only)
  • Dashboard
  • Events
  • Event Expense
  • Event Expense - Approval
  • Event Income
  • Event Ticket
  • Files
  • Notifications
  • Offer
  • Tasks
  • Vendors
  • Venues

Xero — removed from Promoters (2026-06-07); use Backend-Kisum-Finance for OAuth, sync, and invoice PDFs.

For full route group listing, see:


If a request fails:

Is the token present?

Is the token issued by Auth and still valid?

Is x-org present?

Is x-org the intended company?

Does the user belong to that company?

Does the user have the promoter module?

Does the user have the specific promoter.* permission?

Is Auth reachable for effective access resolution?


To call Base successfully, always think:

Auth first
Company context second
Base business route third

If any of those are wrong, the request should fail.


16. Middle-agent marketplace (Artists network BFF)

Section titled “16. Middle-agent marketplace (Artists network BFF)”

Promoters discover non-direct artist access via the Artists network BFF — separate from direct booking-requests.

Promoter UIBFF routeNotes
Marketplace browse (All / Exclusive / Secondary)GET /api/artists-network/marketplace/artist-availabilitiesUnified shelf; official=true/false on each row
Marketplace listing detail + calendarGET /api/artists-network/marketplace/artists/:artistId/listings/:listingId (+ /calendar, /allowed-territories)Same upstream listing resolver for official + secondary; permission promoter.booking.marketplace.view
Marketplace → Non-direct tab (legacy list alias)GET /api/artists-network/middle-agent/availability-listingsPublished availability posts; prefer unified browse endpoint for new work
Send enquiry sheetPOST /api/artists-network/middle-agent/enquiriesBody includes availabilityListingId; creates middle-agent incoming offer upstream
Middle-agent enquiries outboxGET /api/artists-network/middle-agent/enquiriesBuyer-safe status only (source_status stripped)

Permissions: promoter.middle_agent.listing.view, .enquiry.create, .enquiry.view.

Platform doc: Middle Agent Workspace. Package plan: Backend-Kisum-Promoters/MIDDLE_AGENT_WORKSPACE.md.