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
1. Purpose of this document
Section titled “1. Purpose of this document”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:
2. Basic integration idea
Section titled “2. Basic integration idea”You do not log in through Base Backend.
You:
- authenticate through Auth Backend
- obtain a JWT
- choose the active company
- 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.modulesincludes any ofpromoter,promoters,basic_promoter, orbasic_promoters; legacy object shape may still exposebasic) - checks permission
- allows or denies the route
3. Required headers
Section titled “3. Required headers”Every protected request must include:
Authorization: Bearer <JWT_FROM_AUTH_SERVICE>x-org: <COMPANY_ID>Content-Type: application/jsonx-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.
3.1 Authorization
Section titled “3.1 Authorization”Must be a token issued by Auth.
3.2 x-org
Section titled “3.2 x-org”Must be the active company ID for the request.
3.3 Content-Type
Section titled “3.3 Content-Type”Usually application/json, unless the route uses uploads.
4. End-to-end caller flow
Section titled “4. End-to-end caller flow”Step 1 — Authenticate with Auth Backend
Section titled “Step 1 — Authenticate with Auth Backend”The user signs in through Auth, not Base.
Result:
- JWT token
- authenticated user session
Step 2 — Resolve who the user is
Section titled “Step 2 — Resolve who the user is”Caller can use Auth-side identity endpoints as needed.
Step 3 — Choose active company
Section titled “Step 3 — Choose active company”Frontend-Kisum-Promoters does not auto-select a company on login when multiple Promoters-eligible tenants exist. After Auth returns a JWT:
-
The app loads
/dashboard(normal shell). Branch onpromoterEligibleCompaniesfrom the unscopedGET /users/init:- 0 eligible → Subscribe 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+ eligible → Promoters overlay (main pane): pick entitled company only → scoped init.
- 0 eligible → Subscribe overlay (main pane): pick a membership without Promoters → cookie →
-
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. -
Self-serve company delete (no package): on
/profile/companies,TENANT_SUPERADMINmay delete an empty company shell viaGET|DELETE /api/users/companies/:id(+ delete-preview). Nox-org. See promoters API §Profile companies. -
Where the eligible list comes from (changed 2026-07-27): BFF matches Auth
package, CorebasePackage(e.g.promoters,basic_promoters), and module entitlements — not only exactpromoter. It is served from two places:- Unscoped
GET /users/init(nox-org) — still embedspromoterEligibleCompanies, 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-orgset) deliberately omitspromoterEligibleCompanies. 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. - Unscoped
That value is then sent on every tenant-scoped BFF call as:
x-org: <COMPANY_ID>Step 4 — Call Base Backend
Section titled “Step 4 — Call Base Backend”Send the business request with:
- JWT
x-org
Step 5 — Base enforces access
Section titled “Step 5 — Base enforces access”Base validates:
- JWT
x-org- effective access from Auth
promotermodule (or package aliases:promoters,basic_promoter,basic_promoters)- permission
4.1 Artists marketplace BFF (Phase 1)
Section titled “4.1 Artists marketplace BFF (Phase 1)”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:
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.
Artist profile bio_short (Postgres)
Section titled “Artist profile bio_short (Postgres)”- Source of truth:
Backend-Kisum-Artistsartist_profilesviaGET|PUT /api/v1/artists/{id}/profileon the Artists service (persona operators) or platform Admin — not the Promoters BFF. - Promoters BFF:
GET /api/artists-network/artists/{id}/profileonly (read-only proxy). Removed from Promoters (2026-07-14):PUTprofile,POST .../ensure-short-bio,POST .../ensure-bio-formatted, legacyPOST /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.
Offline promoter offers (2026-07-17)
Section titled “Offline promoter offers (2026-07-17)”Ownership: Backend-Kisum-Promoters Postgres (promoters_db, Prisma model offline_offers). Not proxied to Artists; no orchestrateMiddleAgentEnquiryBody or forced event creation on save.
| Method | Path | Permission |
|---|---|---|
POST | /api/booking/offline-offers | promoter.booking.offers.create |
GET | /api/booking/offline-offers | promoter.booking.offers.view |
GET | /api/booking/offline-offers/:id | promoter.booking.offers.view |
PATCH | /api/booking/offline-offers/:id | promoter.booking.offers.create |
DELETE | /api/booking/offline-offers/:id | promoter.booking.offers.create |
- Tenant scope: canonical Core company UUID from
x-org(coreCompanyIdcolumn). artistIdis required (numeric Artists directory id). OptionalavailabilityListingIdis stored as a reference only.- Status defaults to
offline. Full wizard payload is stored inoffersheetJSON. - Env:
DATABASE_URL→promoters_db; runnpx prisma migrate deployafter 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):
| Shelf | Browse source | listingId at enquire |
|---|---|---|
| Exclusive | artist_availabilities (official: true) | availability window UUID |
| Secondary | middle_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/:listingIdredirects 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 (
showDatesquery + 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) → ArtistsGET /api/v1/marketplace/middle-agent-availability-listings/:id(upstream resolver accepts officialartist_availabilitiesUUID or middle-agent listing UUID). Response includesofficial/representation_labelso the UI branches correctly. - Legacy BFF
/api/artists-network/middle-agent/availability-listings/:idremains for backwards compatibility; new Promoters UI uses/marketplace/...only.
4.2 Canonical event identity (Phase 2)
Section titled “4.2 Canonical event identity (Phase 2)”Since 2026-05-27, Promoter events expose a stable UUID for cross-persona references:
| Field | Stored | API alias |
|---|---|---|
publicEventId | Mongo 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
_idunchanged). - 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/:eventIdX-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)”| Route | Purpose |
|---|---|
GET /api/events/:id/booking-marketplace | Lineup artists + filtered middle-agent listings and exclusive avails per artistsDirectoryId (Postgres artists.id) |
GET /api/events/:id/lineup | Returns 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 → BFF | booking/event-link-orchestrator.js validates/creates events + tours before Artists enquiry POST/PATCH (required for offers) |
POST /api/v2/events lineup | Each 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-requests | Optional 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.
4.3 Venues marketplace BFF (Phase 3)
Section titled “4.3 Venues marketplace BFF (Phase 3)”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:
| Field | Example | Use |
|---|---|---|
venue_id (catalog) | 104736 | Marketplace 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 forwardsvisibility=publicso only marketplace-safe packages are returned embedded on each slot (no flat slotprice_centsafter Venue migration000031).GET /api/venues/bookings/:id/activity?venueCompanyId=— promoter-visible booking timeline (status changes + outbound venue communications); requirespromoter.venue.booking.viewand parent booking scope preflight (mirrors artists-network booking activity BFF).POST /api/venues/bookingsbodyvenueId,bookingDate,slotIds,slotPackageSelections([{ slotId, packageId }], one public package per slot; required whenslotIdspresent)
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:
curl -X GET 'http://localhost:3099/api/venues/marketplace/venues/12345' \ -H 'Authorization: Bearer <JWT>' \ -H 'x-org: <PROMOTER_COMPANY_ID>'Example marketplace browse:
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):
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):
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).
4.4 Legacy market retirement (Phase 4)
Section titled “4.4 Legacy market retirement (Phase 4)”Since 2026-05-27, writes to local Mongo market routes return HTTP 410 Gone with error.code: legacy_market_write_removed:
| Legacy write prefix | Use 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.
4.5 Promoter CRM ownership (Phase 5)
Section titled “4.5 Promoter CRM ownership (Phase 5)”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 Promoters | Does 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/notifications | Venue 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:
| Surface | Endpoint | Owner | Purpose |
|---|---|---|---|
| Current plan + active add-ons | GET /api/companies/active-package | Promoters BFF → Core subscription-summary | Read-only commercial state for x-org |
| Available add-ons catalog | GET /api/catalog/addons?audience=promoter (Next.js proxy → Core /public/addons) | Promoters FE → Core | Public catalog, no auth; same-origin avoids Core CORS on local app origins |
| Add / replace payment method | ${CHECKOUT}/billing/add-card?companyId=…&returnTo=… | System-Kisum-Checkout | Card capture only happens inside Checkout |
| Add add-on | ${CHECKOUT}/billing/upgrade?companyId=…&addonKey=…&returnTo=… | System-Kisum-Checkout → Core POST /internal/companies/{id}/addons | Free add-ons provision immediately; paid add-ons use Xendit + finalize |
| Cancel add-on | POST ${CHECKOUT}/api/billing/upgrade/cancel { companyId, addonKey } | System-Kisum-Checkout → Core | Cross-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 toSystem-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/billinguses 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/cancelwith{ companyId, addonKey }.
- Browse → cart → checkout:
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:
| Flag | Where | Purpose |
|---|---|---|
NEXT_PUBLIC_MOCK_BILLING_CHECKOUT=true | Persona frontend | Proceed to Checkout stays in the cart drawer; shows a fake card form |
MOCK_BILLING_CHECKOUT_ENABLED=true | Persona BFF (Artists/Promoters API or Venues Next server) | Enables POST /api/billing/mock-checkout/complete |
MOCK_BILLING_CHECKOUT_AUDIENCE | BFF (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
404when 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:
- 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 taggedsource: 'api'; the manual form warns when a ticket already receives API sales). - 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. - 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. - 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 = ESTIMATEDbecause 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_typeisPAID | COMP(renamed fromSALE). A comp is a sale of inventory — what separates the two is whether money was charged. Comps carrycomp_reasonandissued_by, consumecomp_capacityonly (already carved out oftotal_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_authoritydecides, 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
feesfield 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 unknownmapping_versionis refused, never silently swapped.
5. Example request flow
Section titled “5. Example request flow”5.1 Example: list events
Section titled “5.1 Example: list events”Request
Section titled “Request”curl -X GET 'http://localhost:3099/api/events?type=all&page=1&limit=20' -H 'Authorization: Bearer <JWT>' -H 'x-org: <COMPANY_ID>'What must happen
Section titled “What must happen”- token valid
- company exists in user access scope
promotermodule enabled- user has event view permission
Typical outcome
Section titled “Typical outcome”- success →
200 - bad token →
401 - bad company →
400or403 - no access →
403 - access resolution unavailable →
503
6. Common request patterns
Section titled “6. Common request patterns”6.1 GET
Section titled “6.1 GET”curl -X GET 'http://localhost:3099/api/artists' -H 'Authorization: Bearer <JWT>' -H 'x-org: <COMPANY_ID>'6.2 POST JSON
Section titled “6.2 POST JSON”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" }'6.3 PUT JSON
Section titled “6.3 PUT JSON”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" }'6.4 DELETE
Section titled “6.4 DELETE”curl -X DELETE 'http://localhost:3099/api/agencies/{id}' -H 'Authorization: Bearer <JWT>' -H 'x-org: <COMPANY_ID>'6.5 Multipart upload
Section titled “6.5 Multipart upload”curl -X POST 'http://localhost:3099/api/files/upload' -H 'Authorization: Bearer <JWT>' -H 'x-org: <COMPANY_ID>' -F 'file=@example.pdf'7. What juniors must remember
Section titled “7. What juniors must remember”7.1 Do not log in through Base
Section titled “7.1 Do not log in through Base”Base is not the login service.
7.2 Do not omit x-org
Section titled “7.2 Do not omit x-org”If the route is tenant-scoped, x-org is required.
7.3 Do not guess company
Section titled “7.3 Do not guess company”The UI or calling layer must know the active company and send it explicitly.
7.4 Do not trust frontend UI
Section titled “7.4 Do not trust frontend UI”Just because a button is visible or hidden does not mean the backend will allow the route.
7.5 Do not call deprecated groups
Section titled “7.5 Do not call deprecated groups”Do not use:
- Auth
- Users
- Company Users
- Invitations
- Teams
- Packages
- Permissions
- Role
- Subscription
Those moved to Auth/Core.
8. Error-handling guide
Section titled “8. Error-handling guide”8.0 410 Gone — removed legacy routes
Section titled “8.0 410 Gone — removed legacy routes”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 (useGET /api/users/permissionson Promoters for compat reads)POST|PUT|PATCH|DELETE /api/companies→ Core (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.
8.1 400 Bad Request
Section titled “8.1 400 Bad Request”Usually means:
- missing
x-org - malformed
x-org - bad request payload
8.2 401 Unauthorized
Section titled “8.2 401 Unauthorized”Usually means:
- missing token
- invalid token
- expired token
- token rejected during validation
8.3 403 Forbidden
Section titled “8.3 403 Forbidden”Usually means:
- membership not valid for
x-org promotermodule missing- permission missing
8.4 503 Service Unavailable
Section titled “8.4 503 Service Unavailable”Usually means:
- Base could not resolve effective access from Auth
- Auth service unavailable
- network failure / timeout between services
8.5 Important rule
Section titled “8.5 Important rule”503 must not become temporary access.
The request must fail closed.
9. Integration anti-patterns
Section titled “9. Integration anti-patterns”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
10. Example frontend flow
Section titled “10. Example frontend flow”A typical frontend flow should look like this:
- User signs in with Auth
- Frontend stores current JWT securely
- Frontend gets or already knows active company
- Frontend calls Base with JWT +
x-org - If route denied:
- show proper error
- do not guess fallback
- If
503:- show service unavailable / retry flow
- do not fake access
11. Example backend-to-backend flow
Section titled “11. Example backend-to-backend flow”If another internal backend calls Base on behalf of a user:
- obtain or forward a valid Auth-issued JWT
- send explicit
x-org - call Base route
- respect
401/403/503 - do not add local bypass logic
12. Permission mapping examples
Section titled “12. Permission mapping examples”Examples of route-level access expectations:
GET /dashboard→promoter.dashboard.viewGET /artists→promoter.artist.viewPOST /artists→promoter.artist.createPUT /artists/{id}→promoter.artist.editDELETE /artists/{id}→promoter.artist.deleteGET /events→promoter.event.viewPOST /events→promoter.event.createPUT /events/{id}→promoter.event.editDELETE /events/{id}→promoter.event.deleteGET /vendors→promoter.vendor.viewPOST /vendors→promoter.vendor.createGET /venues→promoter.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:
14. Debugging checklist
Section titled “14. Debugging checklist”If a request fails:
Check 1
Section titled “Check 1”Is the token present?
Check 2
Section titled “Check 2”Is the token issued by Auth and still valid?
Check 3
Section titled “Check 3”Is x-org present?
Check 4
Section titled “Check 4”Is x-org the intended company?
Check 5
Section titled “Check 5”Does the user belong to that company?
Check 6
Section titled “Check 6”Does the user have the promoter module?
Check 7
Section titled “Check 7”Does the user have the specific promoter.* permission?
Check 8
Section titled “Check 8”Is Auth reachable for effective access resolution?
15. Final practical rule
Section titled “15. Final practical rule”To call Base successfully, always think:
Auth firstCompany context secondBase business route thirdIf 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 UI | BFF route | Notes |
|---|---|---|
| Marketplace browse (All / Exclusive / Secondary) | GET /api/artists-network/marketplace/artist-availabilities | Unified shelf; official=true/false on each row |
| Marketplace listing detail + calendar | GET /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-listings | Published availability posts; prefer unified browse endpoint for new work |
| Send enquiry sheet | POST /api/artists-network/middle-agent/enquiries | Body includes availabilityListingId; creates middle-agent incoming offer upstream |
| Middle-agent enquiries outbox | GET /api/artists-network/middle-agent/enquiries | Buyer-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.