Skip to content

Middle Agent Workspace

Middle agents (brokers, regional intermediaries) offer artists they do not exclusively represent. Kisum models them with private workspace artist cards and a dual-sided offer pipeline (buyer vs source) without allowing false official representation claims.

Entitlement (Option B — shipped 2026-06-26)

Section titled “Entitlement (Option B — shipped 2026-06-26)”
LayerRequirement
CoreActive middle_agency addon → enabledModules includes middle_agency
AuthUser grants on middle_agency.* (intersection with company modules)
Artists workspaceorganization_type{middle_agent, hybrid} for operator nav/API
Promoter buyerStandard promoter booking permissions (promoter.artist.offer.view / .edit) — does not require middle_agency addon on the buyer org

Gate order: JWT + canonical x-org → Core addon → Auth permission → workspace type (Artists operator only).

Phase 1 purchase path: Admin assign (POST /api/v1/admin/companies/:id/addons, addonKey: middle_agency). Checkout SKU deferred.

Direct agency flows (booking-requests, official roster, claims) do not require the addon.

DataOwner
Workspace profile, roster cards, offers, chains, commissionsBackend-Kisum-Artists PostgreSQL
Official artists, representation claimsBackend-Kisum-Artists (existing directory)
PermissionsBackend-Kisum-Auth (middle_agency.*, module_key=middle_agency)
Module entitlementBackend-Kisum-Coremiddle_agency addon (company_addons)
Payments / settlement truthBackend-Kisum-Finance
Promoter browser APIBackend-Kisum-Promoters BFF /api/artists-network/middle-agent/* (buyer) and /api/artists-network/middle-agent-workspace/* (operator)
  1. Admin assigns Middle Agency addon to the org; user re-logins.
  2. Set organization_type = middle_agent on workspace profile (or use onboarding at /profile/middle-agent-workspace in Promoters).
  3. Create WorkspaceArtist cards (artist_record_scope = workspace_private).
  4. Manage offers with separate buyer_status and source_status.
  5. Optional: publish listing → promoter marketplace.

Apps:

Persona shellBrowser contractUpstream
Frontend-Kisum-ArtistsDirect GET/PATCH /api/v1/middle-agent/* (403 without addon + approved company claim)Artists BE
Frontend-Kisum-Promoters/middle-agent/* operator desk → BFF /api/artists-network/middle-agent-workspace/* (403 without addon; no source-field redaction on operator routes)Artists BE

Promoter org bridge (2026-07-09): Promoter-only orgs with middle_agency addon but no approved artists_company_claims row are auto-provisioned on first operator API call (ResolveOrProvisionCompanyIDByOrg): inserts a middle_agent_operators row keyed by Core core_company_id (organization_type=middle_agent, display_name=Middle Agent …). No fake companies directory row and no claim bridge. Real agencies keep their directory companies row; the operator row links via directory_company_id. All middle_agent_* tables use company_idmiddle_agent_operators(id).

Operator lifecycle (internal, optional): POST /internal/middle-agent-operators/{orgID}/deactivate|activate flips middle_agent_operators.status (active/inactive). Access remains gated by the Core middle_agency entitlement guard; Admin/Core caller wiring is a follow-up.

Buyer vs operator in Promoters: /booking/* middle-agent flows (marketplace Secondary tab, offers inbox, enquiry wizard) stay on /api/artists-network/middle-agent/* and remain promoter-safe (redacted). /middle-agent/* is the broker operator desk only.

  1. Open Booking → Marketplace (no Middle Agency addon required on the promoter org).
  2. Browse GET /api/artists-network/marketplace/artist-availabilities (BFF → Artists unified GET /api/v1/marketplace/artist-availabilities). One response includes both exclusive (official: true) and secondary (official: false) rows; UI tabs filter client-side or via official=true|false. Legacy GET .../middle-agent/availability-listings still works (proxies unified with official=false). Deprecated workspace-card GET .../middle-agent/listings.
  3. POST .../middle-agent/enquiries with availabilityListingId and structured offersheet (wizard v1) → middle-agent incoming offer tied to the published listing. Full payload stored in middle_agent_offers.metadata_json.offersheet; legacy columns (event_name, venue_name, territory_text, country_id, city_id, buyer_price_*, tour dates) derived for inbox compatibility.
  4. Track GET .../middle-agent/enquiries → Artists GET /api/v1/marketplace/middle-agent-enquiries (buyer org scoped via metadata.buyerCoreCompanyId).
  5. Declined/cancelled offersPATCH / DELETE .../middle-agent/enquiries/:id to revise (resubmit as incoming) or soft-delete; revision logged in middle_agent_offer_activity (buyer_offer_revised, buyer_offer_deleted).
PieceRoute / behavior
Detail enrichGET .../middle-agent-enquiries/:id returns sellerCompanyName, officialArtistId, imageUrl, timeline, revisionHistory (from activity + listing/metadata joins)
RevisePATCH /api/v1/marketplace/middle-agent-enquiries/:id — only when buyer_status{declined, closed_lost, cancelled, lost}; full offersheet required; sets buyer_status=incoming
DeleteDELETE .../:idhard delete (row removed from middle_agent_offers; gone from agency + promoter lists). Only when buyer_status{declined, closed_lost, cancelled, lost}.
BFFPromoters `PATCH
Promoter UI/booking/offers/[id] — artist image + profile link (target=_blank), seller name, venue profile link when shows[].venueId is set; Edit & resubmit / Delete on declined offers; revision history card
Edit wizard/booking/marketplace/enquire?listingId=…&editOfferId=… prefills offersheet; editOfferId alone resolves listing from stored enquiry
MA operator visibilitybuyer_offer_revised appears in existing Activity tab (GET /middle-agent/offers/:id/activity)

Offersheet venue link: store shows[].venueId (+ venueName). Links go to /venues/{venueId} (promoter venue detail). Marketplace route /venues/marketplace/* is for browse/booking flow only — not offer/enquiry links.

Structured offersheet enquiry (2026-06-25)

Section titled “Structured offersheet enquiry (2026-06-25)”
PieceRoute / storage
Allowed territoriesGET /api/v1/marketplace/middle-agent-availability-listings/:id/allowed-territories — BFF GET .../middle-agent/availability-listings/:id/allowed-territories
Geo citiesBFF GET /api/artists-network/geo/countries/:id/cities?q= → Artists GET /api/v1/countries/:id/cities
Venue search (country-scoped)Promoters searchMarketplaceVenues({ countryIso2 }) → Venues admin iso2 filter
Platform T&CGET /api/v1/marketplace/offersheet-terms?version=2026-06-v1 — returns platformTerms (Kisum platform disclaimer) and performanceOfferTerms (performance-offer clauses). Top-level markdown remains performance-offer terms for backward-compatible clients.
Org templatespromoter_offersheet_templates (buyer_core_company_uuid = promoter x-org); CRUD under /api/v1/marketplace/offersheet-templates
Promoter UIFrontend-Kisum-Promoters — 10-step wizard in enquiry sheet; settings at /booking/offer-templates
MA read viewFrontend-Kisum-ArtistsOffersheetSummary on /middle-agent/offers/:id from metadata_json.offersheet

Offersheet schema version: 2026-06-v1 — shows (listing-bound countries), fee (total or per-row), key-details matrix, clause sections (default + append + optional replace HTML), additional conditions, T&C acceptance (terms.accepted, version, timestamp).

Artist binding (2026-06-26, updated 2026-06-30): Promoter enquiry UI does not allow editing the artist name. Each submit carries offersheet.targets[] with { listingId, officialArtistId, artistDisplayName } for the listing being enquired on; officialArtistId is always Postgres public.artists.id (numeric string) — not artists.uuid. Optional eventTitle appends to the derived eventName (e.g. Tyga — Asia Tour 2026). Artists BE validates target listing/artist against the published listing and resolves event_name from targets server-side.

Date mode (2026-06-26): When listing flexibleDates is true (open / flexible routing), step 1 shows tour window start/end and optional per-show dates. When the listing has fixed availableFrom / availableTo, those pickers are hidden and the offer uses the listing dates on submit.

Multi-show tour link (2026-07-15): When the offersheet has 2+ shows (or eventLinkMode=tour), Promoters BFF creates one promoter event per show, then groups them in a tour (EventGroupCommand). Each created event stores Postgres artists.id in lineup (from offersheet.targets[].officialArtistId). Tour creation validates and hydrates lineup through Artists directory — not legacy Mongo artists ObjectIds. event_group.description is optional (empty string default) — marketplace tour auto-create does not supply one.

App: Frontend-Kisum-PromotersNon-direct tab always visible; merged Offers inbox includes middle-agent rows (tagged Middle agent).

Promoter booking interest from artist profile (not marketplace listings) uses booking_requests + MA fan-out — ADR-compliant (does not extend booking_offers).

StepRoute / behavior
CreatePromoter POST /api/artists-network/booking-requests → Artists auto-routes: approved representation → routing_mode=direct_agency + target_company_id; else routing_mode=middle_agent_broadcast + fan-out
Fan-outOne middle_agent_offers row per eligible MA workspace (offer_origin=open_request_broadcast, buyer_status=incoming); metadata links bookingRequestId, buyerCoreCompanyId, artist snapshot
MA inboxGET /api/v1/middle-agent/open-requests (+ detail, POST .../:id/quote simple fee/currency/note)
Promoter responsesGET .../booking-requests/:id includes broadcastResponses + responseCount; quotes appear in merged GET .../middle-agent/enquiries (Offers inbox)
AcceptPOST /api/v1/marketplace/middle-agent-enquiries/:id/accept → BFF POST .../middle-agent/enquiries/:id/accept; closes sibling broadcast offers + fulfills booking request
WithdrawPOST .../booking-requests/:id/withdraw sets request status cancelled and closes linked broadcast MA offers
DeleteDELETE .../booking-requests/:id — promoter hard-delete when status is cancelled (legacy withdrawn rows also allowed until migrated)

Migration: Artists 000023booking_requests.routing_mode; extends middle_agent_offers.offer_origin CHECK for open_request_broadcast.

UI: Promoter Request booking on /artists/[id] (city/country-first). MA Open requests nav in Frontend-Kisum-Artists. Marketplace direct avails no longer expose Send request (profile is the entry point).

Verified direct/exclusive agencies receive official marketplace offers through the normal booking workflow:

  • GET /api/v1/booking-offers and /booking-offers/{id} under normal Artist/agency access.
  • Agency inbox (2026-07-15): incoming exclusive offers also appear in /booking-requests (Booking inbox UI) with a nav badge from GET /api/v1/booking-inbox/summary; list filter GET /api/v1/booking-offers?inbox=incoming&channel=exclusive. On submit, NotifyNewExclusiveMarketplaceOffer emails configured recipients (MA_ENQUIRY_NOTIFY_EMAILS / EMAIL_FROM) — same pattern as middle-agent enquiry notifications.
  • No middle_agent_operators row, Middle Agent addon, or middle_agency.* permission is required.
  • The agency may counter, accept, or decline. Acceptance creates the normal confirmed booking that unlocks contract, touring, and logistics workspaces.
  • Auto-provisioned paperwork on accept (2026-07-15): accepting an exclusive offer now best-effort auto-creates, right after the booking, a per-booking logistics workspace, a tour plan + stop, a DRAFT contract (only when the agency already has a contract template; skipped until one is uploaded), and — when the offer carries a structured payment plan — a receivable Finance invoice for the agency (POST /api/income, one term per installment, amount = sum of installments). Promoters capture the plan in the offer wizard Fee step (offersheet.paymentPlan.installments[]); Artists posts the invoice with Authorization: Bearer <FINANCE_INTERNAL_API_KEY> + x-org, gated on env FINANCE_ARTISTS_INCOME_PATH_TEMPLATE. All steps never send signature emails, are idempotent (no-ops once logistics exists), and can never fail the accept (best-effort, panic-recovered). Middle-agent confirm now runs the SAME paperwork — booking + DRAFT contract + receivable invoice — MINUS touring/logistics (the only exclusive-vs-MA difference). The contract + invoice logic is shared (internal/artists/paperwork). MA bookings originate from a middle_agent_offer (migration 000039: bookings.middle_agent_offer_id, nullable offer_id); the operator’s middle_agent_operators.core_company_id bills the invoice and directory_company_id owns the contract (pure MA desks have none → contract skipped). A MA offer with no official artist creates no booking/contract but still invoices. Idempotency: bookings.middle_agent_offer_id unique + middle_agent_offers.canonical_booking_id + metadata_json.autoInvoiceCreated.
  • Seller resolution (2026-07-15): the receiving agency is resolved from the availability’s creating org (artist_availabilities.created_by_core_company_id) via its approved artists_company_claims company, preferring an artist-specific approved artist_representation_claims when one exists. It does not require an is_exclusive representation claim to exist — an official availability an agency publishes for an artist it owns/manages (with no formal exclusive claim) still routes to that agency’s booking_offers inbox, and the resolved company satisfies the IsCompanyOwnedByOrg accept-side check. Resolution is NULL-safe: a genuinely unrepresented availability degrades to a clear 400, never a 500. Shared officialAvailabilitySellerJoin backs both the marketplace listing card and the offer enquiry.

GET /api/v1/middle-agent/inbound-source-offers remains a Middle Agent source/partner surface and must not be used as the Exclusive agency inbox.

  • Server redacts source_fee, margin, commission without middle_agency.offer.* financial keys.
  • BFF strips the same fields for promoters on buyer routes (/api/artists-network/middle-agent/*).
  • Operator BFF (/api/artists-network/middle-agent-workspace/*) does not redact margin/source fields — entitled operator users receive full deal-desk data per middle_agency.offer.* grants.
  • Artists BE operator routes / Promoters BFF operator surfaces return “Middle Agency addon is required” when Core entitlement missing on the caller org. Marketplace buyer browse/enquiry routes accept promoter booking permissions without that addon.
  • Cannot set representation_type to direct/exclusive without approved representation claim.
  1. Discover Core company UUIDs from Artists DB (middle_agent_operators, middle_agent_offers).
  2. Run Backend-Kisum-Core/scripts/grandfather_middle_agency_addon.sql.
  3. Re-run Auth migrate-permissions; users re-login.

Availability listings + guest intake (2026-06-25 completion)

Section titled “Availability listings + guest intake (2026-06-25 completion)”
FlowBrowser / API
Listing wizardFrontend-Kisum-Artists/middle-agent/listings/new (5-step: Artist → Territory → Dates → Economics → Review; overlap check via GET /middle-agent/availability-listings/overlap-check)
Listing detail (edit)Frontend-Kisum-Artists/middle-agent/listings/:id — read-only summary by default; Edit opens single-page form (artist, territory picker, dates, event types, fees, commission, expiry). PATCH /api/v1/middle-agent/availability-listings/:id. Overlap warning uses GET .../overlap-check?excludeListingId=:id. Published rows keep share-intake actions on the same page (view mode). Delete (next to Edit) when status is draft or archived (not published); DELETE /api/v1/middle-agent/availability-listings/:id.
Directory artist searchGET /api/v1/middle-agent/directory/artists?q= — global directory (not agency roster); returns imageUrl, name, embedded country; typeahead skips COUNT(*) unless includeTotal=true; backed by migration 000018 (pg_trgm on artists.name / sort_name)
Link A offer shareMint on listing share (operator desk on Promoters or Artists) → guest URL /form/offer/{token} on Fullstack-Kisum-Public (no Kisum login — isolated /form/* sub-app with Kisum app-shell chrome; browser calls same-origin /api/form/* BFF only). BFF proxies token routes to **`GET
Link B offer share/middle-agent/intake-links org token mint with optional allowedArtistIds
Token mgmt`GET
Inbox summaryGET /api/v1/middle-agent/inbox-summary → unread incoming count for nav badge
Marketplace reportPromoter POST /api/artists-network/middle-agent/availability-listings/:id/report → Artists marketplace report
Channels (MA-CHANNEL-2)POST /middle-agent/channel-integrations/:channel/connect; public webhooks `GET
EmailArtists SES via EMAIL_ENABLED, SES_*, MA_ENQUIRY_NOTIFY_EMAILS on new enquiry
SEOGuest intake routes: noindex metadata + X-Robots-Tag: noindex

The public guest link (/form/offer/{token} on Fullstack-Kisum-Public) for a listing token hosts the full structured offersheet wizard — the same offersheet a logged-in promoter sends, but unauthenticated and without any Kisum event (guests have no events). A guest submission becomes a real middle_agent_offers row (offer_origin = external_offer, legacy rows may use external_intake; buyer_status = incoming) with the complete offersheet in metadata_json.offersheet, landing in the seller’s offers inbox exactly like an authenticated enquiry.

ConcernBehaviour
Host UIFullstack-Kisum-Public — isolated /form/* layout with canonical Kisum app-shell (DESIGN.md §6): logo row, SideNavRail, Register (header slot → www.kisum.io/pricing), Log in (footer slot → app.kisum.io). No company switcher, no module menu. Two-column enquire page (locked listing context + wizard card).
Browser APISame-origin only: /api/form/offer/{token}/*, /api/geo/*. No Artists/Venues base URLs or internal keys in the browser bundle.
WizardFullstack-Kisum-Public guest-mode copy of the offer-enquiry wizard at src/components/form/offer/offer-enquiry-wizard/. 11 steps: Your details (Name, Company, Address, Email, Country, Phone/WhatsApp — all required) → Shows → Fee → Key details → Marketing/PR → VIP → Special provisions → Exclusivity → Payment schedule → Additional → Review. No org context, no promoter-event linkage (shows use country picker + venue search + free-text city/venue). Browser-local drafts (2026-06-30): multi-draft localStorage per offer token (kisum:guest-offer-drafts:{token}), OfferEnquiryDraftsPanel below locked listing (same UX as Promoters — load/delete/new offer; form starts blank until a draft is picked); cleared on successful submit. Not synced server-side. Guest address + country ride in IntakeSubmitInput and are stored in the offer’s metadata_json guest block.
SubmitPOST /api/v1/public/offer/{token} (legacy /public/middle-agent/intake/{token}) body extended with optional offersheet (the OffersheetPayload shape). Backend runs the same pipeline as the authenticated path — validateOffersheet(…, requireEventLinks=false), validateOffersheetTargets, legacy-field derivation, mergeOffersheetMetadata — then createOfferWithAvailabilityListing. Backward compatible: a contact-only submission (no offersheet) still works.
Public readsGET /api/v1/public/offer/{token}/territories (allowed countries for the listing) and GET …/{token}/offersheet-terms (platformTerms + performanceOfferTerms markdown blocks; top-level markdown = performance-offer terms for legacy clients). Guest review step requires both acceptances; public submit validates offersheet.platformTerms.accepted when requireEventLinks=false. GET …/{token}/venues — token-gated proxy to Venues GET /internal/admin/venues (machine VENUE_INTERNAL_API_KEY; query params search, iso2, city, page, limit; always includes sleeping venues). Guest offersheet venue picker uses this route — browsers never see the Venues internal key. The Your details step’s Country dropdown + phone dial-code come from GET /api/v1/public/countries (Artists geo SoT; returns dial = phone_prefix and flag = flag_emoji alongside code/name). Fee step currency uses GET /api/v1/public/currencies — distinct currency_code values from the Artists countries table; priority order USD, EUR, GBP, AUD, SGD, CNY, then A–Z; default USD. Public Form BFF proxies both geo routes at /api/geo/public-countries and /api/geo/public-currencies. Clause/key-detail defaults are version-stamped client constants (2026-06-v1, matching backend OffersheetPlatformVersion).
Representation-agnosticThe offer GET payload surfaces sellerOrganizationType (middle_agent / agency / hybrid) and the listing’s true representationLabel. Guest UI copy distinguishes middle-agent vs official agency (disclaimer, seller field label, representation badge). Agency orgs publish listings with representation_label = exclusive + official disclaimer. Legacy /form/middle-agent/{token} redirects to /form/offer/{token}. Fail-safe (2026-06-29): when neither sellerOrganizationType nor representationLabel gives a clear signal, the guest UI resolves an unknown seller kind — neutral copy + subject_to_approval badge, never treated as official — instead of silently defaulting to middle-agent.
Guest IP (2026-06-29)The Fullstack-Kisum-Public BFF forwards the guest’s real IP (X-Forwarded-For / X-Real-IP) when proxying the submit to Artists. The backend SubmitPublicOffer resolves it via clientIPFromCtx (forwarded header → c.IP() fallback) so per-IP rate limiting and the stored audit IP reflect the guest, not the BFF’s egress IP. Rate-limit key leads with the token hash, so a spoofed header can’t widen the per-token ceiling.
Guest locked context UIGET …/offer/{token} includes imageUrl on the listing artist and each allowedArtists[] row (directory headshot + profile JSON fallbacks). The guest page shows a circular avatar beside the artist name linking to the Promoters directory profile at https://app.kisum.io/artists/{uuid} — one artist as a title row, multiple as cards. Listing dates display as d MMM, yyyy (date only, no time).
Validation gaterequireEventLinks flag on validateOffersheet: authenticated promoter/marketplace callers pass true (must link Kisum events); the public guest path passes false.

Org tokens (Link B) have no single listing, so they keep the lightweight contact + city-rows form (offersheet submission requires a listing token).

Env (channels + email): WHATSAPP_APP_ID, WHATSAPP_VERIFY_TOKEN, GMAIL_OAUTH_CLIENT_ID, GMAIL_OAUTH_REDIRECT_URI, ARTISTS_PUBLIC_BASE_URL, MA_ENQUIRY_NOTIFY_EMAILS.

Migrations (run per env): Artists 000014000020; Auth 000015 + permission reseed.

Availability listing economics (2026-06-26)

Section titled “Availability listing economics (2026-06-26)”
FieldStorageNotes
eventTypesmiddle_agent_availability_listings.event_types_jsonOptional multi-select. Allowed values: concert, private_corporate, club, festival, other. Same API for middle_agent, agency, and hybrid workspace org types. Legacy single eventType accepted on write only.
artistFeefee_terms_json.artistFee (number)Optional numeric fee; marketplace redaction exposes artistFee (not internal commission). Legacy feeBandText still accepted on read for older rows.
feeTerms.notesfee_terms_json.notesFree-text fee notes.
commissioncommission_jsonInternal; stripped on promoter marketplace reads.

Create/update body: AvailabilityListingInput.eventTypes (array) + feeTerms.artistFee + territories (array of region:{id} / subregion:{id} / country:{id} keys from the territory picker) + optional countryId when a single country is selected.

Listings search (operator workspace, 2026-06-26)

Section titled “Listings search (operator workspace, 2026-06-26)”
SurfaceBehavior
GET /api/v1/middle-agent/availability-listingsOperator list (company-scoped). status: published (UI default), unpublished (draft + archived), or exact draft / archived / published. q matches official artist name (not territory text). Territory filters: regionId, subregionId, countryId (bigint geo ids). Filters combine with AND when multiple territory params are sent.
GET /api/v1/middle-agent/availability-listings/territory-facetsReturns { countries, regions, subregions } derived from this company’s listings only. Country dropdown in UI is limited to countries from this endpoint. Legacy rows with only territory_text are matched by country name for facets + filters.
Frontend-Kisum-Artists /middle-agent/listingsStatus filter defaults to Published; Unpublished & drafts shows draft + archived. Two search modes: Artist name (text) or Territory (region / subregion / country selects). Modes are mutually exclusive on apply.