Skip to content

Backend Core API Contract (OpenAPI-Style)

Related documentation: Kisum System · Backend modules · Data ownership · Infrastructure tasks · Error contract

Contract Specification for Platform Core Internal API

Section titled “Contract Specification for Platform Core Internal API”

Audience: backend engineers, platform-admin backend engineers, Auth backend engineers, QA, DevOps, integrators
Status: contract specification
Scope: this document defines the API contract for Platform Core. It focuses on:

  • exact endpoints
  • methods
  • required headers
  • auth model
  • request parameters
  • request bodies
  • response bodies
  • error responses
  • examples
  • integration rules

This is the API contract companion to:

Path parameters such as {companyId} refer to rows in Core.companies.id. Platform Core is the source of truth for the company master row and all commercial state linked to it. The tables that back those APIs (companies, company_subscriptions, company_addons, etc.) must exist as specified in Backend Core (sections 6.0 and 7)—that DDL is mandatory. See also Data ownership and Backend Core (section 6.4).


Platform Core is an internal-only backend service for every route under /internal/*.

It is not intended for direct browser/frontend access except for the small /public/* read-only catalog group added in the 2026-05-20 Trial Flags + Persona Pricing release — see section 0.1 below.

0.1 Public catalog routes (browser-facing, CORS-gated)

Section titled “0.1 Public catalog routes (browser-facing, CORS-gated)”

The /public route group is the one part of Platform Core that browsers may call directly. It exists so the marketing-site /pricing + /create pages can render persona tabs and decide trial-vs-paid signup without an intermediate BFF.

GET /public/packages?audience=promoter|venue[&key=…]
GET /public/addons?audience=promoter|venue[&keys=ai,finance]
  • Auth: none. CORS gated by CORS_PUBLIC_ORIGINS (comma-separated allow-list). When unset, Core falls back to the existing CORS_ORIGIN value so an ops-tuned origin list keeps working without a second env var. Both unset = fail-closed (no browser may reach /public/*).
  • Audience: required query param; 'promoter' or 'venue'. Other values → 400 validation_error.
  • Filters: packages support a single ?key= lookup; addons support ?keys=a,b,c. Both filter to is_active=true.
  • Response shape: slimmed view (publicPackageView / publicAddonView in internal/httpapi/public_catalog.go) — tax_code and internal flags are stripped so a future Core schema addition cannot accidentally leak.
{
"success": true,
"data": {
"packages": [{
"id": "", "key": "basic_promoter", "name": "Basic (Promoter)",
"description": "", "audience": "promoter",
"priceMinor": 199.00, "currency": "USD", "billingInterval": "monthly",
"taxInclusive": false,
"trialEnabled": true, "trialDays": 14,
"regionPricing": [], "modules": ["basic","ai","finance"]
}]
}
}

Every other Core route stays under /internal/* and requires X-Internal-API-Key.

Its main responsibilities are:

  • storing company master records and company profile/business metadata
  • storing company addresses, social links, and company documents
  • returning company commercial entitlements
  • returning catalog definitions (modules/packages/add-ons)
  • storing and updating package/add-on/subscription state
  • bumping entitlement versions for Auth cache invalidation
  • sending Core-owned commercial lifecycle email after successful writes

The current Backend-Kisum-Core service now exposes internal company-management routes in addition to the original catalog and entitlement endpoints. The live route surface is published in core-openapi.yaml and includes:

  • POST /internal/companies
  • GET/PATCH /internal/companies/{companyId}
  • GET/POST /internal/companies/{companyId}/business-units
  • PATCH /internal/business-units/{businessUnitId}
  • GET/PUT /internal/companies/{companyId}/profile
  • GET/POST/PATCH/DELETE company address routes
  • GET/POST/DELETE company social-link routes
  • GET/POST/DELETE company document routes
  • the existing entitlements, subscription-summary, history, Basic, and add-on endpoints

These routes are internal-only and require X-Internal-API-Key matching CORE_INTERNAL_API_KEY.

Core uses AWS SES only for commercial lifecycle email.

Current implemented email triggers:

  • POST /internal/companies
    • sends company-created email when the resulting company profile contains an email
  • PATCH /internal/companies/{companyId}
    • sends company-status-changed email only when status actually changes and the resulting company profile contains an email
  • POST /internal/companies/{companyId}/basic
    • sends base-subscription-updated email when the resulting company profile contains an email
  • POST /internal/companies/{companyId}/addons
    • sends add-on-updated email when the resulting company profile contains an email

Important rules:

  • email is best-effort and asynchronous
  • the PostgreSQL write remains the source of truth
  • SES failure must not roll back the business write
  • no email is sent when the company has no profile email

0.0.1 What the company sub-resource endpoints are for

Section titled “0.0.1 What the company sub-resource endpoints are for”

These endpoints exist because Core owns more than subscriptions. Core also owns the normalized company master structure, so company business metadata does not have to be flattened into one row or mixed into Auth/Base ownership.

PUT /internal/companies/{companyId}/profile

Section titled “PUT /internal/companies/{companyId}/profile”

Purpose:

  • store or replace the company’s descriptive business profile

Use this endpoint when:

  • a company website, phone, timezone, industry, or description must be saved
  • a public company slug or brand logo URL must be saved as first-class profile fields
  • onboarding/admin flows need to enrich the company after the root company row already exists
  • descriptive company metadata should be updated without changing subscriptions or entitlements

Backed by:

  • company_profiles

Why this exists:

  • the root companies row is the commercial identity anchor
  • the profile is optional descriptive metadata
  • profile data changes independently from commercial state

Integration note:

  • the same profile object may also be sent inline during POST /internal/companies
  • during nested company edit payloads, profile is treated as an upsert

GET /internal/companies/{companyId}/business-units

Section titled “GET /internal/companies/{companyId}/business-units”

Purpose:

  • list Core-owned business-unit master rows for one company

Backed by:

  • business_units

Important rule:

  • this returns business-unit master data only
  • it does not return business-unit memberships
  • memberships remain Auth-owned

POST /internal/companies/{companyId}/business-units

Section titled “POST /internal/companies/{companyId}/business-units”

Purpose:

  • create one Core-owned business-unit master row under a company

Backed by:

  • business_units

Important rule:

  • this creates business-unit identity only
  • it does not create memberships

PATCH /internal/business-units/{businessUnitId}

Section titled “PATCH /internal/business-units/{businessUnitId}”

Purpose:

  • update one Core-owned business-unit master row

Backed by:

  • business_units

Important rule:

  • this updates the business-unit master row only
  • business-unit membership remains in Auth

POST /internal/companies/{companyId}/addresses

Section titled “POST /internal/companies/{companyId}/addresses”

Purpose:

  • create a structured address record for the company

Use this endpoint when:

  • the company needs a legal, billing, mailing, or office address
  • the company needs more than one stored address
  • platform staff needs to mark or replace the primary address later

Backed by:

  • company_addresses

Why this exists:

  • addresses are one-to-many structured records
  • addresses should not be stored as free text in the profile
  • address lifecycle is different from profile and entitlement lifecycle

Integration note:

  • addresses may also be sent inline during POST /internal/companies
  • when nested addresses are sent during company edit, the list is synchronized against company_addresses
  • rows with id are updated, rows without id are inserted, and rows omitted from the submitted list are removed

POST /internal/companies/{companyId}/social-links

Section titled “POST /internal/companies/{companyId}/social-links”

Purpose:

  • register one external/public company link

Use this endpoint when:

  • the company has official Instagram, LinkedIn, website-like public profile links, or similar branded destinations
  • admin/onboarding flows need a normalized list of public links

Backed by:

  • company_social_links

Why this exists:

  • a company may have many links
  • links are repeatable structured records
  • they should not be flattened into a single text field in the profile

Integration note:

  • social links may also be sent inline during POST /internal/companies
  • when nested social links are sent during company edit, the list is synchronized against company_social_links
  • rows with id are updated, rows without id are inserted, and rows omitted from the submitted list are removed

POST /internal/companies/{companyId}/documents

Section titled “POST /internal/companies/{companyId}/documents”

Purpose:

  • register one company-owned file or document reference

Use this endpoint when:

  • legal/compliance/reference company documents need to be linked to the company
  • another internal system uploads a file and Core must track that relationship
  • platform staff needs a structured document registry under the company

Backed by:

  • company_documents

Why this exists:

  • documents are one-to-many records
  • the actual binary may live in external storage while Core stores the association and metadata
  • document lifecycle is separate from company profile text or subscription state

Integration note:

  • documents may also be sent inline during POST /internal/companies
  • when nested documents are sent during company edit, the list is synchronized against company_documents
  • rows with id are updated, rows without id are inserted, and rows omitted from the submitted list are removed

Platform Core is the commercial entitlement backend.

It is positioned in the runtime architecture as follows:

  • Auth Backend = identity, memberships, grants, permissions, delegation
  • Platform Core Backend = packages, add-ons, subscriptions, company entitlements
  • Base / Module Backends = business logic and enforcement
  • Frontend = consumes resolved access for UX, but is not the source of truth

Platform Core does not compute final user access.

The main access rule remains:

effective_access = company_entitlements ∩ membership_grants

Where:

  • company_entitlements come from Platform Core
  • membership_grants come from Auth
  • final effective_access is computed by Auth

Platform Core must not be used by frontend or business backends as a replacement for Auth when making user-level access decisions.

Platform Core only answers:

What does this company commercially own right now?

It does not answer:

  • whether a specific user can use a module
  • whether a specific permission is granted
  • whether a specific actor can delegate access

Platform Core does not define the canonical tenant header for runtime access enforcement. That standard is x-org, defined by Auth/Base/module integration contracts.

Frontend should not call Platform Core directly

Section titled “Frontend should not call Platform Core directly”

Frontend should normally call:

  • Auth Backend
  • Base Backend
  • module backends
  • admin backend (where applicable)

Frontend should not call Platform Core directly to determine access.

The primary caller of Platform Core is:

  • Auth Backend

Especially for:

  • GET /auth/me/access
  • company-switch access refresh
  • access recomputation after entitlement changes

Allowed secondary callers:

  • Platform Admin Backend
  • approved internal jobs/services

Business backends such as Base / Finance / Market / Touring / Venue / AI should not call Platform Core to compute final user authorization.

If a business backend needs user-level authorization, it must use Auth-resolved access context, not Core entitlements directly.

Frontend
-> Auth
-> Auth DB
-> Platform Core
<- resolved access result

This keeps Auth as the access aggregation layer and keeps Core focused on commercial entitlement truth.


To avoid confusion across teams, use these terms consistently:

  • Basic Package → commercial product / subscription concept
  • Core App → the frontend/base application experience
  • Basic Backend → business backend for core platform features
  • Platform Core Backend → commercial entitlement backend

This avoids mixing:

  • Basic Package
  • Basic Backend
  • Platform Core Backend

which are not the same thing.


https://core.kisum.io

All routes in this spec are internal routes.

Allowed callers:

  • Auth Backend
  • Platform Admin Backend
  • approved internal jobs/services

Not allowed:

  • public frontend
  • public mobile apps
  • arbitrary public clients

Platform Core is an internal service in the middle of an access-resolution chain.

Typical runtime chain:

Frontend -> Auth -> Platform Core

or for admin changes:

Platform Admin UI -> Platform Admin Backend -> Platform Core

Business backends should not bypass Auth and use Core as a user-authorization oracle.


All request bodies use:

Content-Type: application/json

unless explicitly noted otherwise.

All successful responses return:

{
"success": true,
"data": {}
}

All error responses return:

{
"success": false,
"error": {
"code": "string_code",
"message": "Human readable message"
}
}

Common error codes used by Platform Core:

  • unauthorized
  • forbidden
  • validation_error
  • not_found
  • conflict
  • not_ready
  • internal_error
  • service_unavailable

All /internal/* routes must require internal service authentication.

Recommended first implementation:

X-Internal-API-Key: <secret>

Alternative future implementation:

Authorization: Bearer <internal-service-token>

If internal auth is missing or invalid:

  • return 401 unauthorized
  • do not expose data
  • do not leak internal routing details
{
"success": false,
"error": {
"code": "unauthorized",
"message": "missing or invalid internal credentials"
}
}

{
"id": "uuid",
"key": "finance",
"name": "Finance",
"type": "addon",
"description": "Finance module",
"isActive": true
}
  • id: UUID
  • key: canonical module key
  • name: display name
  • type: base or addon
  • description: optional description
  • isActive: boolean

{
"id": "uuid",
"key": "basic",
"name": "Basic",
"description": "Basic subscription that enables Core App",
"isActive": true,
"modules": ["basic"]
}
  • id
  • key
  • name
  • description
  • isActive
  • modules: array of module keys currently mapped to this package

{
"id": "uuid",
"key": "finance",
"name": "Finance",
"description": "Finance add-on",
"isActive": true,
"modules": ["finance"]
}

{
"key": "finance",
"status": "active",
"startsAt": "2026-04-16T00:00:00Z",
"endsAt": "2026-05-16T00:00:00Z"
}

{
"companyId": "cmp_001",
"hasBasic": true,
"basePackage": "basic",
"addons": [
{
"key": "finance",
"status": "active",
"startsAt": "2026-04-16T00:00:00Z",
"endsAt": "2026-05-16T00:00:00Z"
}
],
"enabledModules": ["basic", "finance"],
"entitlementVersion": 8,
"updatedAt": "2026-04-16T05:00:00Z"
}

{
"id": "uuid",
"changeType": "addon_activated",
"entityType": "addon",
"entityKey": "finance",
"previousStatus": "inactive",
"newStatus": "active",
"source": "platform_admin",
"changedBy": "admin_user_uuid",
"createdAt": "2026-04-16T05:00:00Z"
}

Simple liveness probe.

No internal auth required, unless your infra policy requires all routes behind private network anyway.

No body.

{
"success": true,
"data": {
"status": "ok"
}
}

Readiness probe.

Must verify:

  • Core DB reachable
  • required config loaded
  • service initialized

No body.

{
"success": true,
"data": {
"status": "ready"
}
}
{
"success": false,
"error": {
"code": "not_ready",
"message": "core database unavailable"
}
}

Return full module catalog.

Internal auth required.

  • Platform Admin Backend
  • Auth Backend (optional)
  • internal jobs

None.

No body.

{
"success": true,
"data": {
"modules": [
{
"id": "11111111-1111-1111-1111-111111111111",
"key": "basic",
"name": "Core App",
"type": "base",
"description": "Core App / Basic product module",
"isActive": true
},
{
"id": "22222222-2222-2222-2222-222222222222",
"key": "finance",
"name": "Finance",
"type": "addon",
"description": "Finance module",
"isActive": true
}
]
}
}
  • 401 unauthorized
  • 500 internal_error

6.2 GET /internal/catalog/modules/{moduleId}

Section titled “6.2 GET /internal/catalog/modules/{moduleId}”

Return one module definition by id.

Internal auth required.

  • Platform Admin Backend
  • Auth Backend (optional)
  • internal jobs
  • moduleId - required UUID

No body.

{
"success": true,
"data": {
"id": "22222222-2222-2222-2222-222222222222",
"key": "finance",
"name": "Finance",
"type": "addon",
"description": "Finance module",
"isActive": true
}
}
  • 400 validation_error
  • 401 unauthorized
  • 404 not_found
  • 500 internal_error

Return full package catalog.

Internal auth required.

No body.

{
"success": true,
"data": {
"packages": [
{
"id": "33333333-3333-3333-3333-333333333333",
"key": "basic",
"name": "Basic",
"description": "Basic subscription that enables Core App",
"isActive": true,
"priceMinor": 99.00,
"currency": "USD",
"billingInterval": "monthly",
"taxCode": "digital_services",
"taxInclusive": false,
"trialDays": 14,
"regionPricing": [],
"modules": ["basic"]
}
]
}
}
  • 401 unauthorized
  • 500 internal_error

6.4 GET /internal/catalog/packages/{packageId}

Section titled “6.4 GET /internal/catalog/packages/{packageId}”

Return one package definition by id.

Internal auth required.

  • packageId - required UUID

No body.

{
"success": true,
"data": {
"id": "33333333-3333-3333-3333-333333333333",
"key": "basic",
"name": "Basic",
"description": "Basic subscription that enables Core App",
"isActive": true,
"priceMinor": 99.00,
"currency": "USD",
"billingInterval": "monthly",
"taxCode": "digital_services",
"taxInclusive": false,
"trialDays": 14,
"regionPricing": [],
"modules": ["basic"]
}
}
  • 400 validation_error
  • 401 unauthorized
  • 404 not_found
  • 500 internal_error

Return full add-on catalog.

Internal auth required.

No body.

{
"success": true,
"data": {
"addons": [
{
"id": "44444444-4444-4444-4444-444444444444",
"key": "finance",
"name": "Finance",
"description": "Finance add-on",
"isActive": true,
"priceMinor": 49.00,
"currency": "USD",
"billingInterval": "monthly",
"taxCode": "digital_services",
"taxInclusive": false,
"trialDays": 7,
"regionPricing": [],
"modules": ["finance"]
},
{
"id": "55555555-5555-5555-5555-555555555555",
"key": "market",
"name": "Market",
"description": "Market add-on",
"isActive": true,
"modules": ["market"]
}
]
}
}
  • 401 unauthorized
  • 500 internal_error

6.6 GET /internal/catalog/addons/{addonId}

Section titled “6.6 GET /internal/catalog/addons/{addonId}”

Return one add-on definition by id.

Internal auth required.

  • addonId - required UUID

No body.

{
"success": true,
"data": {
"id": "44444444-4444-4444-4444-444444444444",
"key": "finance",
"name": "Finance",
"description": "Finance add-on",
"isActive": true,
"priceMinor": 49.00,
"currency": "USD",
"billingInterval": "monthly",
"taxCode": "digital_services",
"taxInclusive": false,
"trialDays": 7,
"regionPricing": [],
"modules": ["finance"]
}
}
  • 400 validation_error
  • 401 unauthorized
  • 404 not_found
  • 500 internal_error

Return a paginated internal admin list of companies.

This route is intended for:

  • admin company pickers
  • platform-admin company overview screens
  • internal tools that need company master data without loading each company one by one

Each row includes:

  • company → the full Core company master row
  • profile → the company profile row when it exists
  • addresses → all addresses for that company

This route does not include:

  • social links
  • documents
  • entitlements
  • subscription summary
  • history

Internal auth required.

NameTypeRequiredBehavior
pageintegernodefaults to 1 when omitted or < 1
limitintegernodefaults to 20 when omitted or < 1; maximum 100

No body.

GET /internal/companies?page=1&limit=20
X-Internal-API-Key: <secret>
Accept: application/json
{
"success": true,
"data": {
"companies": [
{
"company": {
"id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"kisumCompanyId": "6800f4d9c1a2b3c4d5e6f789",
"legalName": "Kisum Entertainment Group Pte Ltd",
"displayName": "Kisum Entertainment Group",
"status": "active",
"createdSource": "internal",
"billingProvider": "stripe",
"billingCustomerId": "cus_123456789",
"metadata": {
"region": "SG"
},
"createdAt": "2026-04-18T10:00:00Z",
"updatedAt": "2026-04-18T10:00:00Z"
},
"profile": {
"companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"slug": "kisum-entertainment",
"logoUrl": "https://files.kisum.dev/companies/kisum-entertainment/logo.png",
"website": "https://kisum.dev",
"email": "hello@kisum.dev",
"phone": "+65 6123 4567",
"timezone": "Asia/Singapore",
"industry": "Entertainment",
"description": "Artist management and touring operations",
"metadata": {
"preferredLanguage": "en"
}
},
"addresses": [
{
"id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
"companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"type": "primary",
"line1": "120 Orchard Road",
"line2": "Level 15",
"city": "Singapore",
"state": "Singapore",
"postalCode": "238888",
"country": "Singapore",
"isPrimary": true
}
]
}
],
"total": 1,
"page": 1,
"limit": 20
}
}
  • ordering is newest-first by companies.created_at DESC
  • profile is omitted when no profile row exists
  • addresses is an empty array when no address rows exist
  • this is an admin list endpoint, not a full company bundle endpoint

Return the Core company master row only.

Internal auth required.

No body.

{
"success": true,
"data": {
"id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"kisumCompanyId": "6800f4d9c1a2b3c4d5e6f789",
"legalName": "Kisum Entertainment Group Pte Ltd",
"displayName": "Kisum Entertainment Group",
"status": "active",
"createdSource": "internal",
"billingProvider": "stripe",
"billingCustomerId": "cus_123456789",
"metadata": {
"region": "SG"
},
"createdAt": "2026-04-18T10:00:00Z",
"updatedAt": "2026-04-18T10:00:00Z"
}
}

Create the root company row and, optionally, the full nested company structure in one request.

Internal auth required.

  • Platform Admin Backend
  • approved internal provisioning or migration jobs

legalName is required. All nested sections are optional.

{
"legalName": "Kisum Entertainment Group Pte Ltd",
"displayName": "Kisum Entertainment Group",
"status": "active",
"createdSource": "internal",
"billingProvider": "stripe",
"billingCustomerId": "cus_123456789",
"metadata": {
"region": "SG"
},
"profile": {
"website": "https://group.kisum.dev",
"email": "group@kisum.dev",
"phone": "+65 6999 8888",
"timezone": "Asia/Singapore",
"industry": "Entertainment",
"description": "Regional artist management and touring group",
"metadata": {
"preferredLanguage": "en"
}
},
"addresses": [
{
"type": "primary",
"line1": "120 Orchard Road",
"line2": "Level 15",
"city": "Singapore",
"state": "Singapore",
"postalCode": "238888",
"country": "Singapore",
"isPrimary": true
}
],
"socialLinks": [
{
"platform": "instagram",
"label": "Official Instagram",
"url": "https://instagram.com/kisumgroup"
}
],
"documents": [
{
"type": "business_registration",
"name": "Business Registration Certificate",
"storageKey": "companies/company-id/business-registration.pdf",
"url": "https://files.kisum.dev/companies/company-id/business-registration.pdf",
"mimeType": "application/pdf",
"sizeBytes": 251004,
"metadata": {
"version": 1
}
}
]
}
  • legalName is required
  • id, if provided, must be a valid UUID
  • addresses[].line1 is required for every address
  • socialLinks[].platform and socialLinks[].url are required
  • documents[].type and documents[].name are required
  • creates the companies row first
  • upserts profile into company_profiles when provided
  • inserts addresses into company_addresses
  • inserts socialLinks into company_social_links
  • inserts documents into company_documents
  • everything runs in one transaction

Response returns the full stored bundle:

{
"success": true,
"data": {
"company": {
"id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"legalName": "Kisum Entertainment Group Pte Ltd",
"displayName": "Kisum Entertainment Group",
"status": "active",
"createdSource": "internal",
"createdAt": "2026-04-18T10:00:00Z",
"updatedAt": "2026-04-18T10:00:00Z"
},
"profile": {
"companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"website": "https://group.kisum.dev",
"email": "group@kisum.dev",
"phone": "+65 6999 8888",
"timezone": "Asia/Singapore",
"industry": "Entertainment",
"description": "Regional artist management and touring group",
"metadata": {
"preferredLanguage": "en"
},
"createdAt": "2026-04-18T10:00:00Z",
"updatedAt": "2026-04-18T10:00:00Z"
},
"addresses": [],
"socialLinks": [],
"documents": []
}
}

Partially update the root company row and optionally synchronize nested company collections.

Internal auth required.

  • Platform Admin Backend
  • approved internal provisioning tools

Any top-level field is optional.

{
"displayName": "Kisum Entertainment Group",
"profile": {
"website": "https://group.kisum.dev",
"email": "group@kisum.dev",
"phone": "+65 6999 8888",
"timezone": "Asia/Singapore",
"industry": "Entertainment",
"description": "Regional artist management and touring group",
"metadata": {
"preferredLanguage": "en"
}
},
"addresses": [
{
"id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
"type": "primary",
"line1": "120 Orchard Road",
"line2": "Level 15",
"city": "Singapore",
"state": "Singapore",
"postalCode": "238888",
"country": "Singapore",
"isPrimary": true
},
{
"type": "legal",
"line1": "1 Marina Boulevard",
"line2": "Suite 18-01",
"city": "Singapore",
"state": "Singapore",
"postalCode": "018989",
"country": "Singapore",
"isPrimary": false
}
]
}
  • omit a collection key to leave that collection unchanged
  • send addresses: [] to clear all addresses
  • send socialLinks: [] to clear all social links
  • send documents: [] to clear all documents
  • for nested collections:
    • rows with id are updated
    • rows without id are inserted
    • rows omitted from a submitted collection are deleted
  • only send the sections that the user actually edited
  • do not send empty arrays by accident
  • for single-item delete actions, subresource DELETE endpoints are still the clearest choice

Response returns the full stored bundle after patch:

{
"success": true,
"data": {
"company": {
"id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"legalName": "Kisum Entertainment Group Pte Ltd",
"displayName": "Kisum Entertainment Group",
"status": "active",
"createdSource": "internal",
"createdAt": "2026-04-18T10:00:00Z",
"updatedAt": "2026-04-18T11:00:00Z"
},
"profile": {
"companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"website": "https://group.kisum.dev",
"email": "group@kisum.dev",
"phone": "+65 6999 8888",
"timezone": "Asia/Singapore",
"industry": "Entertainment",
"description": "Regional artist management and touring group",
"metadata": {
"preferredLanguage": "en"
},
"createdAt": "2026-04-18T10:00:00Z",
"updatedAt": "2026-04-18T11:00:00Z"
},
"addresses": [],
"socialLinks": [],
"documents": []
}
}

6.7 GET /internal/companies/{companyId}/profile

Section titled “6.7 GET /internal/companies/{companyId}/profile”

Return the current company profile row.

Internal auth required.

No body.

{
"success": true,
"data": {
"companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"slug": "kisum-entertainment-group",
"logoUrl": "https://files.kisum.dev/companies/kisum-entertainment-group/logo.png",
"website": "https://group.kisum.dev",
"email": "group@kisum.dev",
"phone": "+65 6999 8888",
"timezone": "Asia/Singapore",
"industry": "Entertainment",
"description": "Regional artist management and touring group",
"metadata": {
"preferredLanguage": "en"
},
"createdAt": "2026-04-18T10:00:00Z",
"updatedAt": "2026-04-18T11:00:00Z"
}
}

6.8 PUT /internal/companies/{companyId}/profile

Section titled “6.8 PUT /internal/companies/{companyId}/profile”

Create or replace the single descriptive company profile row.

Internal auth required.

{
"slug": "kisum-entertainment-group",
"logoUrl": "https://files.kisum.dev/companies/kisum-entertainment-group/logo.png",
"website": "https://group.kisum.dev",
"email": "group@kisum.dev",
"phone": "+65 6999 8888",
"timezone": "Asia/Singapore",
"industry": "Entertainment",
"description": "Regional artist management and touring group",
"metadata": {
"preferredLanguage": "en"
}
}
  • inserts a new company_profiles row if none exists
  • updates the existing row if one already exists
  • does not affect addresses, social links, documents, subscriptions, or entitlements
{
"success": true,
"data": {
"companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"slug": "kisum-entertainment-group",
"logoUrl": "https://files.kisum.dev/companies/kisum-entertainment-group/logo.png",
"website": "https://group.kisum.dev",
"email": "group@kisum.dev",
"phone": "+65 6999 8888",
"timezone": "Asia/Singapore",
"industry": "Entertainment",
"description": "Regional artist management and touring group",
"metadata": {
"preferredLanguage": "en"
},
"createdAt": "2026-04-18T10:00:00Z",
"updatedAt": "2026-04-18T11:00:00Z"
}
}

6.9 GET /internal/companies/{companyId}/addresses

Section titled “6.9 GET /internal/companies/{companyId}/addresses”

List all company addresses.

Internal auth required.

No body.

{
"success": true,
"data": {
"addresses": [
{
"id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
"companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"type": "primary",
"line1": "120 Orchard Road",
"line2": "Level 15",
"city": "Singapore",
"state": "Singapore",
"postalCode": "238888",
"country": "Singapore",
"isPrimary": true,
"createdAt": "2026-04-18T10:00:00Z",
"updatedAt": "2026-04-18T10:00:00Z"
}
]
}
}

6.10 POST /internal/companies/{companyId}/addresses

Section titled “6.10 POST /internal/companies/{companyId}/addresses”

Insert one address row under the company.

{
"type": "primary",
"line1": "120 Orchard Road",
"line2": "Level 15",
"city": "Singapore",
"state": "Singapore",
"postalCode": "238888",
"country": "Singapore",
"isPrimary": true
}
{
"success": true,
"data": {
"id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
"companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"type": "primary",
"line1": "120 Orchard Road",
"line2": "Level 15",
"city": "Singapore",
"state": "Singapore",
"postalCode": "238888",
"country": "Singapore",
"isPrimary": true,
"createdAt": "2026-04-18T10:00:00Z",
"updatedAt": "2026-04-18T10:00:00Z"
}
}

6.11 PATCH /internal/companies/{companyId}/addresses/{addressId}

Section titled “6.11 PATCH /internal/companies/{companyId}/addresses/{addressId}”

Update one company address row.

{
"type": "billing",
"line1": "80 Raffles Place",
"line2": "Unit 20-01",
"city": "Singapore",
"state": "Singapore",
"postalCode": "048624",
"country": "Singapore",
"isPrimary": false
}
{
"success": true,
"data": {
"id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
"companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"type": "billing",
"line1": "80 Raffles Place",
"line2": "Unit 20-01",
"city": "Singapore",
"state": "Singapore",
"postalCode": "048624",
"country": "Singapore",
"isPrimary": false,
"createdAt": "2026-04-18T10:00:00Z",
"updatedAt": "2026-04-18T11:00:00Z"
}
}

6.12 DELETE /internal/companies/{companyId}/addresses/{addressId}

Section titled “6.12 DELETE /internal/companies/{companyId}/addresses/{addressId}”

Delete one company address row.

No body.

{
"success": true,
"data": {
"deleted": true,
"addressId": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
}
}

Section titled “6.13 GET /internal/companies/{companyId}/social-links”

List all company social links.

No body.

{
"success": true,
"data": {
"socialLinks": [
{
"id": "cccccccc-cccc-cccc-cccc-cccccccccccc",
"companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"platform": "instagram",
"label": "Official Instagram",
"url": "https://instagram.com/kisumgroup",
"createdAt": "2026-04-18T10:00:00Z"
}
]
}
}

6.14 POST /internal/companies/{companyId}/social-links

Section titled “6.14 POST /internal/companies/{companyId}/social-links”

Insert one company social-link row.

{
"platform": "instagram",
"label": "Official Instagram",
"url": "https://instagram.com/kisumgroup"
}
{
"success": true,
"data": {
"id": "cccccccc-cccc-cccc-cccc-cccccccccccc",
"companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"platform": "instagram",
"label": "Official Instagram",
"url": "https://instagram.com/kisumgroup",
"createdAt": "2026-04-18T10:00:00Z"
}
}

6.15 DELETE /internal/companies/{companyId}/social-links/{socialLinkId}

Section titled “6.15 DELETE /internal/companies/{companyId}/social-links/{socialLinkId}”

Delete one company social link row.

No body.

{
"success": true,
"data": {
"deleted": true,
"socialLinkId": "cccccccc-cccc-cccc-cccc-cccccccccccc"
}
}

6.16 GET /internal/companies/{companyId}/documents

Section titled “6.16 GET /internal/companies/{companyId}/documents”

List all company documents.

No body.

{
"success": true,
"data": {
"documents": [
{
"id": "dddddddd-dddd-dddd-dddd-dddddddddddd",
"companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"type": "business_registration",
"name": "Business Registration Certificate",
"storageKey": "companies/company-id/business-registration.pdf",
"url": "https://files.kisum.dev/companies/company-id/business-registration.pdf",
"mimeType": "application/pdf",
"sizeBytes": 251004,
"metadata": {
"version": 1
},
"createdAt": "2026-04-18T10:00:00Z",
"updatedAt": "2026-04-18T10:00:00Z"
}
]
}
}

6.17 POST /internal/companies/{companyId}/documents

Section titled “6.17 POST /internal/companies/{companyId}/documents”

Insert one company document row.

{
"type": "business_registration",
"name": "Business Registration Certificate",
"storageKey": "companies/company-id/business-registration.pdf",
"url": "https://files.kisum.dev/companies/company-id/business-registration.pdf",
"mimeType": "application/pdf",
"sizeBytes": 251004,
"metadata": {
"version": 1
}
}
{
"success": true,
"data": {
"id": "dddddddd-dddd-dddd-dddd-dddddddddddd",
"companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"type": "business_registration",
"name": "Business Registration Certificate",
"storageKey": "companies/company-id/business-registration.pdf",
"url": "https://files.kisum.dev/companies/company-id/business-registration.pdf",
"mimeType": "application/pdf",
"sizeBytes": 251004,
"metadata": {
"version": 1
},
"createdAt": "2026-04-18T10:00:00Z",
"updatedAt": "2026-04-18T10:00:00Z"
}
}

6.18 DELETE /internal/companies/{companyId}/documents/{documentId}

Section titled “6.18 DELETE /internal/companies/{companyId}/documents/{documentId}”

Delete one company document row.

No body.

{
"success": true,
"data": {
"deleted": true,
"documentId": "dddddddd-dddd-dddd-dddd-dddddddddddd"
}
}

7.1 GET /internal/companies/{companyId}/entitlements

Section titled “7.1 GET /internal/companies/{companyId}/entitlements”

Return the current normalized commercial entitlement state for one company.

Auth Backend, during /auth/me/access.

This endpoint returns company commercial entitlement state only.

It is intentionally designed for Auth to consume and merge with membership grants.

It must not be treated as a user-access endpoint by callers.

Internal auth required.

  • Auth Backend
  • Platform Admin Backend
  • internal jobs
  • companyId — required UUID

No body.

  • companyId must be valid UUID
  • if company has no active Basic and no active add-ons, response still succeeds with empty entitlement state
{
"success": true,
"data": {
"companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"hasBasic": false,
"basePackage": null,
"addons": [
{
"key": "finance",
"status": "active",
"startsAt": "2026-04-16T00:00:00Z",
"endsAt": "2026-05-16T00:00:00Z"
},
{
"key": "market",
"status": "active",
"startsAt": "2026-04-16T00:00:00Z",
"endsAt": "2026-05-16T00:00:00Z"
}
],
"enabledModules": ["finance", "market"],
"entitlementVersion": 7,
"updatedAt": "2026-04-16T05:00:00Z"
}
}
{
"success": true,
"data": {
"companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"hasBasic": true,
"basePackage": "basic",
"addons": [
{
"key": "finance",
"status": "active",
"startsAt": "2026-04-16T00:00:00Z",
"endsAt": "2026-05-16T00:00:00Z"
}
],
"enabledModules": ["basic", "finance"],
"entitlementVersion": 8,
"updatedAt": "2026-04-16T05:00:00Z"
}
}
{
"success": true,
"data": {
"companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"hasBasic": false,
"basePackage": null,
"addons": [],
"enabledModules": [],
"entitlementVersion": 1,
"updatedAt": "2026-04-16T05:00:00Z"
}
}
{
"success": false,
"error": {
"code": "validation_error",
"message": "invalid companyId"
}
}
{
"success": false,
"error": {
"code": "unauthorized",
"message": "missing or invalid internal credentials"
}
}

Use only if your system requires company existence to be pre-validated in Core.

{
"success": false,
"error": {
"code": "not_found",
"message": "company not found"
}
}
{
"success": false,
"error": {
"code": "internal_error",
"message": "failed to load entitlements"
}
}

7.2 GET /internal/companies/{companyId}/subscription-summary

Section titled “7.2 GET /internal/companies/{companyId}/subscription-summary”

Return admin-friendly company subscription summary.

Internal auth required.

  • Platform Admin Backend
  • internal jobs
  • Auth Backend (optional)

No body.

{
"success": true,
"data": {
"companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"hasBasic": true,
"basePackage": "basic",
"items": [
{
"kind": "package",
"id": "255db6d9-c9bc-4fff-a08f-b97f4906507e",
"key": "basic",
"name": "Basic",
"description": "Basic subscription that enables the core application",
"isActive": true,
"status": "active",
"startsAt": "2026-04-16T00:00:00Z",
"endsAt": "2026-05-16T00:00:00Z",
"priceMinor": 99.00,
"currency": "USD",
"billingInterval": "monthly",
"taxCode": "digital_services",
"taxInclusive": false,
"trialDays": 14,
"regionPricing": [
{
"region": "SG",
"currency": "SGD",
"priceMinor": 129.00
}
],
"entitlementKind": "package",
"entitlementLabel": "Package"
},
{
"kind": "addon",
"id": "b8c37dfb-d154-408a-a7e8-fbfab9ff6a2c",
"key": "finance",
"name": "Finance",
"description": "Finance add-on",
"isActive": true,
"status": "active",
"startsAt": "2026-04-16T00:00:00Z",
"endsAt": "2026-05-16T00:00:00Z",
"priceMinor": 49.00,
"currency": "USD",
"billingInterval": "monthly",
"taxCode": "digital_services",
"taxInclusive": false,
"trialDays": 7,
"regionPricing": [],
"entitlementKind": "addon",
"entitlementLabel": "Add-on"
},
{
"kind": "addon",
"id": "d2f3e7f4-9159-4cfa-85a9-c3d9027d56a1",
"key": "market",
"name": "Market",
"description": "Market add-on",
"isActive": true,
"status": "active",
"startsAt": "2026-04-16T00:00:00Z",
"endsAt": "2026-05-16T00:00:00Z",
"priceMinor": 39.00,
"currency": "USD",
"billingInterval": "monthly",
"taxCode": "digital_services",
"taxInclusive": false,
"trialDays": 0,
"regionPricing": [],
"entitlementKind": "addon",
"entitlementLabel": "Add-on"
}
],
"entitlementVersion": 7
}
}
  • items is the canonical admin-friendly array for UI rendering
  • one row is returned per active commercial assignment:
    • one package row for the active base package, if present
    • one addon row for each active add-on
  • each items[] row includes:
    • catalog identity: id, key, name, description
    • assignment state: status, startsAt, endsAt
    • commercial values: priceMinor (decimal amount), currency, billingInterval, taxCode, taxInclusive, trialDays, regionPricing
  • a company may return only add-on rows and no package row when it has add-ons without an active base package

7.3 GET /internal/companies/{companyId}/history

Section titled “7.3 GET /internal/companies/{companyId}/history”

Return entitlement history for a company.

Internal auth required.

  • Platform Admin Backend
  • internal audit/reporting jobs
  • limit
  • offset

No body.

{
"success": true,
"data": {
"companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"history": [
{
"id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
"changeType": "addon_activated",
"entityType": "addon",
"entityKey": "finance",
"previousStatus": "inactive",
"newStatus": "active",
"source": "platform_admin",
"changedBy": "admin_user_uuid",
"createdAt": "2026-04-16T05:00:00Z"
}
]
}
}

These are usually called by the Platform Admin Backend, not directly from UI.

Create a new module catalog entry.

Internal auth required.

  • Platform Admin Backend only
{
"key": "finance",
"name": "Finance",
"type": "addon",
"description": "Finance module",
"isActive": true
}
  • key required
  • key unique
  • key lowercase slug recommended
  • name required
  • type required and must be base or addon
{
"success": true,
"data": {
"id": "22222222-2222-2222-2222-222222222222",
"key": "finance",
"name": "Finance",
"type": "addon",
"description": "Finance module",
"isActive": true
}
}
{
"success": false,
"error": {
"code": "conflict",
"message": "module key already exists"
}
}

8.2 PATCH /internal/catalog/modules/{moduleId}

Section titled “8.2 PATCH /internal/catalog/modules/{moduleId}”

Update existing module metadata.

Internal auth required.

  • moduleId — required UUID

All fields optional, but at least one required:

{
"name": "Finance",
"description": "Finance module updated",
"isActive": true
}
  • key should be immutable after creation in first version
  • patch should update updated_at
{
"success": true,
"data": {
"id": "22222222-2222-2222-2222-222222222222",
"key": "finance",
"name": "Finance",
"type": "addon",
"description": "Finance module updated",
"isActive": true
}
}

8.3 DELETE /internal/catalog/modules/{moduleId}

Section titled “8.3 DELETE /internal/catalog/modules/{moduleId}”

Delete a module that is not linked to any package or add-on.

  • moduleId — required UUID
{
"success": true,
"data": {
"deleted": true,
"id": "22222222-2222-2222-2222-222222222222"
}
}
  • 400 validation_error
  • 401 unauthorized
  • 404 not_found
  • 409 conflict when the module is still linked to packages or add-ons
  • 500 internal_error

Create a new package.

Internal auth required.

{
"key": "basic",
"name": "Basic",
"description": "Basic subscription that enables Core App",
"isActive": true,
"priceMinor": 99.00,
"currency": "USD",
"billingInterval": "monthly",
"taxCode": "digital_services",
"taxInclusive": false,
"trialDays": 14,
"regionPricing": [
{
"region": "SG",
"currency": "SGD",
"priceMinor": 129.00
}
],
"moduleKeys": ["basic"]
}
  • key unique
  • priceMinor, currency, and billingInterval required; priceMinor is a decimal amount like 199.00
  • billingInterval must be monthly, quarterly, yearly, or one_time
  • trialDays must be >= 0
  • moduleKeys must all exist in modules
  • package may map to one or more modules
{
"success": true,
"data": {
"id": "33333333-3333-3333-3333-333333333333",
"key": "basic",
"name": "Basic",
"description": "Basic subscription that enables Core App",
"isActive": true,
"priceMinor": 99.00,
"currency": "USD",
"billingInterval": "monthly",
"taxCode": "digital_services",
"taxInclusive": false,
"trialDays": 14,
"regionPricing": [],
"modules": ["basic"]
}
}

8.5 PATCH /internal/catalog/packages/{packageId}

Section titled “8.5 PATCH /internal/catalog/packages/{packageId}”

Update package metadata and/or mapping.

  • packageId — UUID
{
"name": "Basic",
"description": "Updated Basic description",
"isActive": true,
"priceMinor": 119.00,
"currency": "USD",
"billingInterval": "yearly",
"trialDays": 30,
"moduleKeys": ["basic"]
}
  • if moduleKeys is present, replace package mapping set with provided keys
  • mapping updates may require entitlement-version impact handling for active companies
{
"success": true,
"data": {
"id": "33333333-3333-3333-3333-333333333333",
"key": "basic",
"name": "Basic",
"description": "Updated Basic description",
"isActive": true,
"priceMinor": 119.00,
"currency": "USD",
"billingInterval": "yearly",
"taxCode": "digital_services",
"taxInclusive": false,
"trialDays": 30,
"regionPricing": [],
"modules": ["basic"]
}
}

8.6 DELETE /internal/catalog/packages/{packageId}

Section titled “8.6 DELETE /internal/catalog/packages/{packageId}”

Delete a package that is not assigned to any company subscription.

  • packageId — required UUID
{
"success": true,
"data": {
"deleted": true,
"id": "33333333-3333-3333-3333-333333333333"
}
}
  • 400 validation_error
  • 401 unauthorized
  • 404 not_found
  • 409 conflict when the package is assigned to companies
  • 500 internal_error

Create a new add-on.

{
"key": "finance",
"name": "Finance",
"description": "Finance add-on",
"isActive": true,
"priceMinor": 49.00,
"currency": "USD",
"billingInterval": "monthly",
"taxCode": "digital_services",
"taxInclusive": false,
"trialDays": 7,
"moduleKeys": ["finance"]
}
  • priceMinor, currency, and billingInterval required; priceMinor is a decimal amount like 199.00
  • add-on mappings may only include modules whose type is addon
{
"success": true,
"data": {
"id": "44444444-4444-4444-4444-444444444444",
"key": "finance",
"name": "Finance",
"description": "Finance add-on",
"isActive": true,
"priceMinor": 49.00,
"currency": "USD",
"billingInterval": "monthly",
"taxCode": "digital_services",
"taxInclusive": false,
"trialDays": 7,
"regionPricing": [],
"modules": ["finance"]
}
}

8.8 PATCH /internal/catalog/addons/{addonId}

Section titled “8.8 PATCH /internal/catalog/addons/{addonId}”

Update add-on metadata and/or mapping.

  • addonId — UUID
{
"name": "Finance",
"description": "Finance updated",
"isActive": true,
"priceMinor": 59.00,
"currency": "USD",
"billingInterval": "yearly",
"moduleKeys": ["finance"]
}
{
"success": true,
"data": {
"id": "44444444-4444-4444-4444-444444444444",
"key": "finance",
"name": "Finance",
"description": "Finance updated",
"isActive": true,
"priceMinor": 59.00,
"currency": "USD",
"billingInterval": "yearly",
"taxCode": "digital_services",
"taxInclusive": false,
"trialDays": 7,
"regionPricing": [],
"modules": ["finance"]
}
}

8.9 DELETE /internal/catalog/addons/{addonId}

Section titled “8.9 DELETE /internal/catalog/addons/{addonId}”

Delete an add-on that is not assigned to any company.

  • addonId — required UUID
{
"success": true,
"data": {
"deleted": true,
"id": "44444444-4444-4444-4444-444444444444"
}
}
  • 400 validation_error
  • 401 unauthorized
  • 404 not_found
  • 409 conflict when the add-on is assigned to companies
  • 500 internal_error

9.1 POST /internal/companies/{companyId}/basic

Section titled “9.1 POST /internal/companies/{companyId}/basic”

Create or update Basic subscription state for a company.

Internal auth required.

  • Platform Admin Backend
  • optional billing/reconciliation jobs
  • companyId — UUID
{
"status": "active",
"startsAt": "2026-04-16T00:00:00Z",
"endsAt": "2026-05-16T00:00:00Z",
"source": "platform_admin",
"externalReference": "sub_123"
}
  • active
  • inactive
  • cancelled
  • expired
  • trial
  • paused
  • resolve package basic
  • upsert company_subscriptions
  • bump entitlementVersion
  • write entitlement history
  • return normalized summary
{
"success": true,
"data": {
"companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"hasBasic": true,
"basePackage": "basic",
"entitlementVersion": 8
}
}
{
"status": "inactive",
"source": "platform_admin"
}

9.2 POST /internal/companies/{companyId}/addons

Section titled “9.2 POST /internal/companies/{companyId}/addons”

Create or update add-on state for one company/add-on pair.

Internal auth required.

  • Platform Admin Backend
  • optional billing/reconciliation jobs
  • companyId — UUID
{
"addonKey": "finance",
"status": "active",
"startsAt": "2026-04-16T00:00:00Z",
"endsAt": "2026-05-16T00:00:00Z",
"source": "platform_admin",
"externalReference": "addon_sub_123"
}
  • resolve addon by addonKey
  • upsert company_addons
  • bump entitlementVersion
  • write entitlement history
  • return normalized addon result
{
"success": true,
"data": {
"companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"addonKey": "finance",
"status": "active",
"entitlementVersion": 9
}
}
{
"addonKey": "finance",
"status": "inactive",
"source": "platform_admin"
}
{
"success": false,
"error": {
"code": "validation_error",
"message": "addonKey is required"
}
}
{
"success": false,
"error": {
"code": "not_found",
"message": "addon not found"
}
}

Core returns entitlementVersion in all company entitlement responses.

Core must bump entitlementVersion when:

  • Basic activated
  • Basic deactivated
  • Basic expired
  • add-on activated
  • add-on deactivated
  • add-on expired
  • package mapping changed in a way affecting active companies
  • add-on mapping changed in a way affecting active companies
  • manual entitlement repair

Auth will use:

access:{companyId}:{membershipId}:{accessVersion}:{entitlementVersion}

or equivalent cache strategy.

Core does not manage Auth cache directly via this API contract.
But Core must always return the correct current version.


All timestamps are ISO-8601 UTC strings, example:

2026-04-16T05:00:00Z
  • basePackage may be null
  • startsAt / endsAt may be null
  • arrays should be empty arrays, not null, unless there is a strong reason otherwise

Responses must be normalized and not leak DB implementation details.

{
"addons": [
{ "key": "finance", "status": "active" }
]
}
{
"addons": [
{ "addon_id": "uuid", "company_addon_status": "A" }
]
}

StatusMeaning
200Successful read/update
201Successful creation
400Validation error
401Missing/invalid internal auth
403Authenticated internal caller but not allowed
404Resource not found
409Conflict
500Internal server error
503Dependency not ready / service unavailable

  1. Frontend calls /auth/me/access
  2. Auth loads user grants from Auth DB
  3. Auth calls:
    • GET /internal/companies/{companyId}/entitlements
  4. Core returns normalized company entitlements
  5. Auth merges:
    • entitlements ∩ membership grants
  6. Auth returns effective access to frontend
  1. Platform Admin UI calls Admin Backend
  2. Admin Backend validates platform-admin access
  3. Admin Backend calls Core internal catalog write endpoint
  4. Core writes DB rows and mapping rows
  5. Core returns normalized object
  1. Platform Admin UI calls Admin Backend
  2. Admin Backend validates admin rights
  3. Admin Backend calls POST /internal/companies/{companyId}/basic or /addons
  4. Core updates state
  5. Core bumps version
  6. Auth cache becomes stale and must rebuild on next access resolution

QA should validate at minimum:

  • GET /health
  • GET /ready
  • unauthorized internal call returns 401
  • module/package/addon reads return normalized arrays
  • company with no Basic but active addon returns valid entitlements
  • company with Basic + addon returns both in enabled modules
  • activating/deactivating Basic bumps entitlementVersion
  • activating/deactivating addon bumps entitlementVersion
  • patching catalog records preserves immutable keys
  • invalid UUID returns validation error
  • unknown addon/module/package returns not found where appropriate

This contract defines how internal services interact with Platform Core.
Core exposes catalog APIs and company entitlement APIs.
Auth reads company entitlements from Core.
Platform Admin writes company commercial state into Core.
Core returns normalized enabled modules and entitlementVersion.
Core does not return user permissions, roles, or grants.
Frontend should not call Core directly for access decisions.
Business backends should not use Core as a user-authorization service.

16. Access model layers (REFERENCE — DO NOT IMPLEMENT HERE)

Section titled “16. Access model layers (REFERENCE — DO NOT IMPLEMENT HERE)”

Platform Core participates in a 3-layer access model but only owns Level 1.

Level 1 — Company entitlements (OWNED BY CORE)

Section titled “Level 1 — Company entitlements (OWNED BY CORE)”

Defines what the company has purchased:

  • Basic
  • Finance
  • Market
  • Touring
  • Venue
  • AI

Source of truth: Platform Core DB


Level 2 — Membership module grants (OWNED BY AUTH)

Section titled “Level 2 — Membership module grants (OWNED BY AUTH)”

Defines which modules a user can access. Core MUST NOT store or evaluate this.


Defines actions inside modules. Core MUST NOT store or evaluate this.


Core returns entitlements ONLY
Core never computes user access

Platform Core is a dependency of Auth.

Auth → Core → Auth → Frontend

Core:

  • returns company entitlements
  • returns enabled modules
  • returns entitlementVersion

Auth:

  • merges entitlements with user grants
  • computes effective access
  • returns final access model

Core must NEVER:

  • return user-level access
  • validate permissions
  • validate roles
  • decide if a request is allowed

Core MUST NOT implement:

effective_access = entitlements ∩ grants

This logic belongs ONLY to Auth.


19. Delegation awareness (READ-ONLY CONTEXT)

Section titled “19. Delegation awareness (READ-ONLY CONTEXT)”

Core is aware that delegation exists, but:

  • does not enforce delegation
  • does not validate delegation
  • does not store delegation rules

Delegation is owned by Auth.


Frontend must NOT call Platform Core directly for access.

Allowed frontend calls:

  • Auth
  • Base backend
  • module backends
  • admin backend

Business backends must NOT:

  • call Core for user access decisions
  • use Core as authorization service

They must rely on:

Auth → /auth/me/access

Core = commercial truth
Auth = access truth
Backend = enforcement
Frontend = UX only