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 unscoped promoterEligibleCompanies:
    • 0 eligibleSubscribe overlay (main pane): pick any membership → cookie → /profile/billing.
    • 1 eligible → auto-set cookie, scoped init, full app.
    • 2+ eligiblePromoters overlay (main pane): pick entitled company → 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. Eligible companies come from one unscoped GET /users/init (promoterEligibleCompanies from BFF).

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-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.

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.

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.bio_short via GET|PUT /api/v1/artists/{id}/profile (bio, bioShort, profileJson).
  • Not legacy Mongo POST /api/artists/:id/bio or https://v1.kisum.ai/bio for Artists-network reads.
  • Promoters BFF: POST /api/artists-network/artists/{id}/profile/ensure-short-bio — when bio_short is empty, Gemini summarizes existing bio (strict plain-text prompt), then PUTs profile upstream. Requires promoter artist edit permission. Idempotent; ?force=true regenerates.
  • Long bio formatting: column artist_profiles.bio_formatted (true | false | null). When not true, POST .../profile/ensure-bio-formatted runs once: Gemini outputs semantic HTML into bio, sets bio_formatted=true, stores original plain text in profile_json.bioSourcePlain. Migration Backend-Kisum-Artists/migrations/20260602_artist_profiles_bio_formatted.sql.
  • Promoters UI: artist detail triggers short-bio ensure in the background; Biography tab triggers bio-format ensure when bio_formatted is not true. Overview shows bioShort only + link to Biography tab.

Frontend (Frontend-Kisum-Promoters): libs/data.ts calls the BFF for marketplace avails (getAvails) and outbound booking pipeline (getOfferSent, createOffer). Login uses NEXT_PUBLIC_AUTH_BASE_URL, not Promoters /api/auth/*.


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.

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.


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

Callers use Promoters (:3099) with promoter JWT + x-org. For venue-operator-scoped reads/writes, also pass venueCompanyId (query/body) or x-venue-org — the BFF forwards that as upstream x-org.

Example marketplace browse:

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

Example booking create with event linkage:

Terminal window
curl -X POST 'http://localhost:3099/api/venues-network/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-network/*
/api/integrations/finance/* (proxy)Subscriptions/packages → Core

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

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 }.

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


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
  • basic 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
  • basic 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 /dashboardbasic.dashboard.view
  • GET /artistsbasic.artist.view
  • POST /artistsbasic.artist.create
  • PUT /artists/{id}basic.artist.edit
  • DELETE /artists/{id}basic.artist.delete
  • GET /eventsbasic.event.view
  • POST /eventsbasic.event.create
  • PUT /events/{id}basic.event.edit
  • DELETE /events/{id}basic.event.delete
  • GET /vendorsbasic.vendor.view
  • POST /vendorsbasic.vendor.create
  • GET /venuesbasic.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 basic module?

Does the user have the specific basic.* 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.