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:
- Backend Implementation → master backend plan
- Backend Auth → Auth backend/API specification
- Backend Core → full engineering/backend specification (including section 6.4 on the mandatory
companiestable in Core DB)
Company paths in this API
Section titled “Company paths in this API”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).
0. API overview
Section titled “0. API overview”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 existingCORS_ORIGINvalue 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 tois_active=true. - Response shape: slimmed view (
publicPackageView/publicAddonViewininternal/httpapi/public_catalog.go) —tax_codeand 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
Runtime implementation note
Section titled “Runtime implementation note”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/companiesGET/PATCH /internal/companies/{companyId}GET/POST /internal/companies/{companyId}/business-unitsPATCH /internal/business-units/{businessUnitId}GET/PUT /internal/companies/{companyId}/profileGET/POST/PATCH/DELETEcompany address routesGET/POST/DELETEcompany social-link routesGET/POST/DELETEcompany 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.
0.0.2 Core email behavior
Section titled “0.0.2 Core email behavior”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
- sends company-created email when the resulting company profile contains an
PATCH /internal/companies/{companyId}- sends company-status-changed email only when
statusactually changes and the resulting company profile contains anemail
- sends company-status-changed email only when
POST /internal/companies/{companyId}/basic- sends base-subscription-updated email when the resulting company profile contains an
email
- sends base-subscription-updated email when the resulting company profile contains an
POST /internal/companies/{companyId}/addons- sends add-on-updated email when the resulting company profile contains an
email
- sends add-on-updated email when the resulting company profile contains an
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
companiesrow 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
idare updated, rows withoutidare 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
idare updated, rows withoutidare 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
idare updated, rows withoutidare inserted, and rows omitted from the submitted list are removed
0.1 Runtime position in the platform
Section titled “0.1 Runtime position in the platform”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
Main runtime rule
Section titled “Main runtime rule”Platform Core does not compute final user access.
The main access rule remains:
effective_access = company_entitlements ∩ membership_grantsWhere:
company_entitlementscome from Platform Coremembership_grantscome from Auth- final
effective_accessis computed by Auth
Important implication
Section titled “Important implication”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
0.2 Request ownership and primary callers
Section titled “0.2 Request ownership and primary callers”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.
Primary caller
Section titled “Primary caller”The primary caller of Platform Core is:
- Auth Backend
Especially for:
GET /auth/me/access- company-switch access refresh
- access recomputation after entitlement changes
Secondary callers
Section titled “Secondary callers”Allowed secondary callers:
- Platform Admin Backend
- approved internal jobs/services
Non-primary callers
Section titled “Non-primary callers”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.
Clean runtime pattern
Section titled “Clean runtime pattern”Frontend -> Auth -> Auth DB -> Platform Core <- resolved access resultThis keeps Auth as the access aggregation layer and keeps Core focused on commercial entitlement truth.
0.3 Naming clarification
Section titled “0.3 Naming clarification”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.
1. Base URL and exposure model
Section titled “1. Base URL and exposure model”1.1 Recommended base URL
Section titled “1.1 Recommended base URL”https://core.kisum.io1.2 Exposure model
Section titled “1.2 Exposure model”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
Integration note
Section titled “Integration note”Platform Core is an internal service in the middle of an access-resolution chain.
Typical runtime chain:
Frontend -> Auth -> Platform Coreor for admin changes:
Platform Admin UI -> Platform Admin Backend -> Platform CoreBusiness backends should not bypass Auth and use Core as a user-authorization oracle.
2. Content type and response conventions
Section titled “2. Content type and response conventions”2.1 Request content type
Section titled “2.1 Request content type”All request bodies use:
Content-Type: application/jsonunless explicitly noted otherwise.
2.2 Success envelope
Section titled “2.2 Success envelope”All successful responses return:
{ "success": true, "data": {}}2.3 Error envelope
Section titled “2.3 Error envelope”All error responses return:
{ "success": false, "error": { "code": "string_code", "message": "Human readable message" }}2.4 Standard error codes
Section titled “2.4 Standard error codes”Common error codes used by Platform Core:
unauthorizedforbiddenvalidation_errornot_foundconflictnot_readyinternal_errorservice_unavailable
3. Internal authentication contract
Section titled “3. Internal authentication contract”3.1 Required authentication
Section titled “3.1 Required authentication”All /internal/* routes must require internal service authentication.
Recommended first implementation:
X-Internal-API-Key: <secret>Alternative future implementation:
Authorization: Bearer <internal-service-token>3.2 Required behavior
Section titled “3.2 Required behavior”If internal auth is missing or invalid:
- return
401 unauthorized - do not expose data
- do not leak internal routing details
Example error
Section titled “Example error”{ "success": false, "error": { "code": "unauthorized", "message": "missing or invalid internal credentials" }}4. Common object schemas
Section titled “4. Common object schemas”4.1 Module object
Section titled “4.1 Module object”{ "id": "uuid", "key": "finance", "name": "Finance", "type": "addon", "description": "Finance module", "isActive": true}Fields
Section titled “Fields”id: UUIDkey: canonical module keyname: display nametype:baseoraddondescription: optional descriptionisActive: boolean
4.2 Package object
Section titled “4.2 Package object”{ "id": "uuid", "key": "basic", "name": "Basic", "description": "Basic subscription that enables Core App", "isActive": true, "modules": ["basic"]}Fields
Section titled “Fields”idkeynamedescriptionisActivemodules: array of module keys currently mapped to this package
4.3 Add-on object
Section titled “4.3 Add-on object”{ "id": "uuid", "key": "finance", "name": "Finance", "description": "Finance add-on", "isActive": true, "modules": ["finance"]}4.4 Company add-on summary
Section titled “4.4 Company add-on summary”{ "key": "finance", "status": "active", "startsAt": "2026-04-16T00:00:00Z", "endsAt": "2026-05-16T00:00:00Z"}4.5 Company entitlement object
Section titled “4.5 Company entitlement object”{ "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"}4.6 Entitlement history object
Section titled “4.6 Entitlement history object”{ "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"}5. Health endpoints
Section titled “5. Health endpoints”5.1 GET /health
Section titled “5.1 GET /health”Purpose
Section titled “Purpose”Simple liveness probe.
No internal auth required, unless your infra policy requires all routes behind private network anyway.
Request body
Section titled “Request body”No body.
Response — 200
Section titled “Response — 200”{ "success": true, "data": { "status": "ok" }}5.2 GET /ready
Section titled “5.2 GET /ready”Purpose
Section titled “Purpose”Readiness probe.
Readiness checks
Section titled “Readiness checks”Must verify:
- Core DB reachable
- required config loaded
- service initialized
Request body
Section titled “Request body”No body.
Success — 200
Section titled “Success — 200”{ "success": true, "data": { "status": "ready" }}Failure — 503
Section titled “Failure — 503”{ "success": false, "error": { "code": "not_ready", "message": "core database unavailable" }}6. Catalog read endpoints
Section titled “6. Catalog read endpoints”6.1 GET /internal/catalog/modules
Section titled “6.1 GET /internal/catalog/modules”Purpose
Section titled “Purpose”Return full module catalog.
Internal auth required.
Who can call
Section titled “Who can call”- Platform Admin Backend
- Auth Backend (optional)
- internal jobs
Query params
Section titled “Query params”None.
Request body
Section titled “Request body”No body.
Success — 200
Section titled “Success — 200”{ "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 } ] }}Errors
Section titled “Errors”401 unauthorized500 internal_error
6.2 GET /internal/catalog/modules/{moduleId}
Section titled “6.2 GET /internal/catalog/modules/{moduleId}”Purpose
Section titled “Purpose”Return one module definition by id.
Internal auth required.
Who can call
Section titled “Who can call”- Platform Admin Backend
- Auth Backend (optional)
- internal jobs
Path params
Section titled “Path params”moduleId- required UUID
Request body
Section titled “Request body”No body.
Success — 200
Section titled “Success — 200”{ "success": true, "data": { "id": "22222222-2222-2222-2222-222222222222", "key": "finance", "name": "Finance", "type": "addon", "description": "Finance module", "isActive": true }}Errors
Section titled “Errors”400 validation_error401 unauthorized404 not_found500 internal_error
6.3 GET /internal/catalog/packages
Section titled “6.3 GET /internal/catalog/packages”Purpose
Section titled “Purpose”Return full package catalog.
Internal auth required.
Request body
Section titled “Request body”No body.
Success — 200
Section titled “Success — 200”{ "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"] } ] }}Errors
Section titled “Errors”401 unauthorized500 internal_error
6.4 GET /internal/catalog/packages/{packageId}
Section titled “6.4 GET /internal/catalog/packages/{packageId}”Purpose
Section titled “Purpose”Return one package definition by id.
Internal auth required.
Path params
Section titled “Path params”packageId- required UUID
Request body
Section titled “Request body”No body.
Success — 200
Section titled “Success — 200”{ "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"] }}Errors
Section titled “Errors”400 validation_error401 unauthorized404 not_found500 internal_error
6.5 GET /internal/catalog/addons
Section titled “6.5 GET /internal/catalog/addons”Purpose
Section titled “Purpose”Return full add-on catalog.
Internal auth required.
Request body
Section titled “Request body”No body.
Success — 200
Section titled “Success — 200”{ "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"] } ] }}Errors
Section titled “Errors”401 unauthorized500 internal_error
6.6 GET /internal/catalog/addons/{addonId}
Section titled “6.6 GET /internal/catalog/addons/{addonId}”Purpose
Section titled “Purpose”Return one add-on definition by id.
Internal auth required.
Path params
Section titled “Path params”addonId- required UUID
Request body
Section titled “Request body”No body.
Success — 200
Section titled “Success — 200”{ "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"] }}Errors
Section titled “Errors”400 validation_error401 unauthorized404 not_found500 internal_error
7. Company read and entitlement endpoints
Section titled “7. Company read and entitlement endpoints”6.4A GET /internal/companies
Section titled “6.4A GET /internal/companies”Purpose
Section titled “Purpose”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
Returned data shape
Section titled “Returned data shape”Each row includes:
company→ the full Core company master rowprofile→ the company profile row when it existsaddresses→ all addresses for that company
This route does not include:
- social links
- documents
- entitlements
- subscription summary
- history
Internal auth required.
Query parameters
Section titled “Query parameters”| Name | Type | Required | Behavior |
|---|---|---|---|
page | integer | no | defaults to 1 when omitted or < 1 |
limit | integer | no | defaults to 20 when omitted or < 1; maximum 100 |
Request body
Section titled “Request body”No body.
Example request
Section titled “Example request”GET /internal/companies?page=1&limit=20X-Internal-API-Key: <secret>Accept: application/jsonSuccess — 200
Section titled “Success — 200”{ "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 profileis omitted when no profile row existsaddressesis an empty array when no address rows exist- this is an admin list endpoint, not a full company bundle endpoint
6.4 GET /internal/companies/{companyId}
Section titled “6.4 GET /internal/companies/{companyId}”Purpose
Section titled “Purpose”Return the Core company master row only.
Internal auth required.
Request body
Section titled “Request body”No body.
Success — 200
Section titled “Success — 200”{ "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" }}6.5 POST /internal/companies
Section titled “6.5 POST /internal/companies”Purpose
Section titled “Purpose”Create the root company row and, optionally, the full nested company structure in one request.
Internal auth required.
Who can call
Section titled “Who can call”- Platform Admin Backend
- approved internal provisioning or migration jobs
Request body
Section titled “Request body”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 } } ]}Validation rules
Section titled “Validation rules”legalNameis requiredid, if provided, must be a valid UUIDaddresses[].line1is required for every addresssocialLinks[].platformandsocialLinks[].urlare requireddocuments[].typeanddocuments[].nameare required
Behavior
Section titled “Behavior”- creates the
companiesrow first - upserts
profileintocompany_profileswhen provided - inserts
addressesintocompany_addresses - inserts
socialLinksintocompany_social_links - inserts
documentsintocompany_documents - everything runs in one transaction
Success — 201
Section titled “Success — 201”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": [] }}6.6 PATCH /internal/companies/{companyId}
Section titled “6.6 PATCH /internal/companies/{companyId}”Purpose
Section titled “Purpose”Partially update the root company row and optionally synchronize nested company collections.
Internal auth required.
Who can call
Section titled “Who can call”- Platform Admin Backend
- approved internal provisioning tools
Request body
Section titled “Request body”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 } ]}Critical PATCH rules
Section titled “Critical PATCH rules”- 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
idare updated - rows without
idare inserted - rows omitted from a submitted collection are deleted
- rows with
Safest frontend rule
Section titled “Safest frontend rule”- only send the sections that the user actually edited
- do not send empty arrays by accident
- for single-item delete actions, subresource
DELETEendpoints are still the clearest choice
Success — 200
Section titled “Success — 200”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”Purpose
Section titled “Purpose”Return the current company profile row.
Internal auth required.
Request body
Section titled “Request body”No body.
Success — 200
Section titled “Success — 200”{ "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”Purpose
Section titled “Purpose”Create or replace the single descriptive company profile row.
Internal auth required.
Request body
Section titled “Request body”{ "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" }}Behavior
Section titled “Behavior”- inserts a new
company_profilesrow if none exists - updates the existing row if one already exists
- does not affect addresses, social links, documents, subscriptions, or entitlements
Success — 200
Section titled “Success — 200”{ "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”Purpose
Section titled “Purpose”List all company addresses.
Internal auth required.
Request body
Section titled “Request body”No body.
Success — 200
Section titled “Success — 200”{ "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”Purpose
Section titled “Purpose”Insert one address row under the company.
Request body
Section titled “Request body”{ "type": "primary", "line1": "120 Orchard Road", "line2": "Level 15", "city": "Singapore", "state": "Singapore", "postalCode": "238888", "country": "Singapore", "isPrimary": true}Success — 201
Section titled “Success — 201”{ "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}”Purpose
Section titled “Purpose”Update one company address row.
Request body
Section titled “Request body”{ "type": "billing", "line1": "80 Raffles Place", "line2": "Unit 20-01", "city": "Singapore", "state": "Singapore", "postalCode": "048624", "country": "Singapore", "isPrimary": false}Success — 200
Section titled “Success — 200”{ "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}”Purpose
Section titled “Purpose”Delete one company address row.
Request body
Section titled “Request body”No body.
Success — 200
Section titled “Success — 200”{ "success": true, "data": { "deleted": true, "addressId": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" }}6.13 GET /internal/companies/{companyId}/social-links
Section titled “6.13 GET /internal/companies/{companyId}/social-links”Purpose
Section titled “Purpose”List all company social links.
Request body
Section titled “Request body”No body.
Success — 200
Section titled “Success — 200”{ "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”Purpose
Section titled “Purpose”Insert one company social-link row.
Request body
Section titled “Request body”{ "platform": "instagram", "label": "Official Instagram", "url": "https://instagram.com/kisumgroup"}Success — 201
Section titled “Success — 201”{ "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}”Purpose
Section titled “Purpose”Delete one company social link row.
Request body
Section titled “Request body”No body.
Success — 200
Section titled “Success — 200”{ "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”Purpose
Section titled “Purpose”List all company documents.
Request body
Section titled “Request body”No body.
Success — 200
Section titled “Success — 200”{ "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”Purpose
Section titled “Purpose”Insert one company document row.
Request body
Section titled “Request body”{ "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 — 201
Section titled “Success — 201”{ "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}”Purpose
Section titled “Purpose”Delete one company document row.
Request body
Section titled “Request body”No body.
Success — 200
Section titled “Success — 200”{ "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”Purpose
Section titled “Purpose”Return the current normalized commercial entitlement state for one company.
Main consumer
Section titled “Main consumer”Auth Backend, during /auth/me/access.
Runtime role of this endpoint
Section titled “Runtime role of this endpoint”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.
Who can call
Section titled “Who can call”- Auth Backend
- Platform Admin Backend
- internal jobs
Path params
Section titled “Path params”companyId— required UUID
Request body
Section titled “Request body”No body.
Validation rules
Section titled “Validation rules”companyIdmust be valid UUID- if company has no active Basic and no active add-ons, response still succeeds with empty entitlement state
Success — 200
Section titled “Success — 200”{ "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" }}Example: Basic + Finance
Section titled “Example: Basic + Finance”{ "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" }}Example: No entitlements
Section titled “Example: No entitlements”{ "success": true, "data": { "companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "hasBasic": false, "basePackage": null, "addons": [], "enabledModules": [], "entitlementVersion": 1, "updatedAt": "2026-04-16T05:00:00Z" }}Errors
Section titled “Errors”400 validation error
Section titled “400 validation error”{ "success": false, "error": { "code": "validation_error", "message": "invalid companyId" }}401 unauthorized
Section titled “401 unauthorized”{ "success": false, "error": { "code": "unauthorized", "message": "missing or invalid internal credentials" }}404 not found
Section titled “404 not found”Use only if your system requires company existence to be pre-validated in Core.
{ "success": false, "error": { "code": "not_found", "message": "company not found" }}500 internal error
Section titled “500 internal error”{ "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”Purpose
Section titled “Purpose”Return admin-friendly company subscription summary.
Internal auth required.
Who can call
Section titled “Who can call”- Platform Admin Backend
- internal jobs
- Auth Backend (optional)
Request body
Section titled “Request body”No body.
Success — 200
Section titled “Success — 200”{ "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 }}itemsis the canonical admin-friendly array for UI rendering- one row is returned per active commercial assignment:
- one
packagerow for the active base package, if present - one
addonrow for each active add-on
- one
- 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
- catalog identity:
- 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”Purpose
Section titled “Purpose”Return entitlement history for a company.
Internal auth required.
Who can call
Section titled “Who can call”- Platform Admin Backend
- internal audit/reporting jobs
Query params (optional)
Section titled “Query params (optional)”limitoffset
Request body
Section titled “Request body”No body.
Success — 200
Section titled “Success — 200”{ "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" } ] }}8. Catalog write endpoints
Section titled “8. Catalog write endpoints”These are usually called by the Platform Admin Backend, not directly from UI.
8.1 POST /internal/catalog/modules
Section titled “8.1 POST /internal/catalog/modules”Purpose
Section titled “Purpose”Create a new module catalog entry.
Internal auth required.
Who can call
Section titled “Who can call”- Platform Admin Backend only
Request body
Section titled “Request body”{ "key": "finance", "name": "Finance", "type": "addon", "description": "Finance module", "isActive": true}Validation rules
Section titled “Validation rules”keyrequiredkeyuniquekeylowercase slug recommendednamerequiredtyperequired and must bebaseoraddon
Success — 201
Section titled “Success — 201”{ "success": true, "data": { "id": "22222222-2222-2222-2222-222222222222", "key": "finance", "name": "Finance", "type": "addon", "description": "Finance module", "isActive": true }}Conflict — 409
Section titled “Conflict — 409”{ "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}”Purpose
Section titled “Purpose”Update existing module metadata.
Internal auth required.
Path params
Section titled “Path params”moduleId— required UUID
Request body
Section titled “Request body”All fields optional, but at least one required:
{ "name": "Finance", "description": "Finance module updated", "isActive": true}keyshould be immutable after creation in first version- patch should update
updated_at
Success — 200
Section titled “Success — 200”{ "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}”Purpose
Section titled “Purpose”Delete a module that is not linked to any package or add-on.
Path params
Section titled “Path params”moduleId— required UUID
Success — 200
Section titled “Success — 200”{ "success": true, "data": { "deleted": true, "id": "22222222-2222-2222-2222-222222222222" }}Errors
Section titled “Errors”400 validation_error401 unauthorized404 not_found409 conflictwhen the module is still linked to packages or add-ons500 internal_error
8.4 POST /internal/catalog/packages
Section titled “8.4 POST /internal/catalog/packages”Purpose
Section titled “Purpose”Create a new package.
Internal auth required.
Request body
Section titled “Request body”{ "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"]}Validation rules
Section titled “Validation rules”keyuniquepriceMinor,currency, andbillingIntervalrequired;priceMinoris a decimal amount like199.00billingIntervalmust bemonthly,quarterly,yearly, orone_timetrialDaysmust be>= 0moduleKeysmust all exist inmodules- package may map to one or more modules
Success — 201
Section titled “Success — 201”{ "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}”Purpose
Section titled “Purpose”Update package metadata and/or mapping.
Path params
Section titled “Path params”packageId— UUID
Request body
Section titled “Request body”{ "name": "Basic", "description": "Updated Basic description", "isActive": true, "priceMinor": 119.00, "currency": "USD", "billingInterval": "yearly", "trialDays": 30, "moduleKeys": ["basic"]}Behavior
Section titled “Behavior”- if
moduleKeysis present, replace package mapping set with provided keys - mapping updates may require entitlement-version impact handling for active companies
Success — 200
Section titled “Success — 200”{ "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}”Purpose
Section titled “Purpose”Delete a package that is not assigned to any company subscription.
Path params
Section titled “Path params”packageId— required UUID
Success — 200
Section titled “Success — 200”{ "success": true, "data": { "deleted": true, "id": "33333333-3333-3333-3333-333333333333" }}Errors
Section titled “Errors”400 validation_error401 unauthorized404 not_found409 conflictwhen the package is assigned to companies500 internal_error
8.7 POST /internal/catalog/addons
Section titled “8.7 POST /internal/catalog/addons”Purpose
Section titled “Purpose”Create a new add-on.
Request body
Section titled “Request body”{ "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"]}Validation rules
Section titled “Validation rules”priceMinor,currency, andbillingIntervalrequired;priceMinoris a decimal amount like199.00- add-on mappings may only include modules whose
typeisaddon
Success — 201
Section titled “Success — 201”{ "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}”Purpose
Section titled “Purpose”Update add-on metadata and/or mapping.
Path params
Section titled “Path params”addonId— UUID
Request body
Section titled “Request body”{ "name": "Finance", "description": "Finance updated", "isActive": true, "priceMinor": 59.00, "currency": "USD", "billingInterval": "yearly", "moduleKeys": ["finance"]}Success — 200
Section titled “Success — 200”{ "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}”Purpose
Section titled “Purpose”Delete an add-on that is not assigned to any company.
Path params
Section titled “Path params”addonId— required UUID
Success — 200
Section titled “Success — 200”{ "success": true, "data": { "deleted": true, "id": "44444444-4444-4444-4444-444444444444" }}Errors
Section titled “Errors”400 validation_error401 unauthorized404 not_found409 conflictwhen the add-on is assigned to companies500 internal_error
9. Company entitlement write endpoints
Section titled “9. Company entitlement write endpoints”9.1 POST /internal/companies/{companyId}/basic
Section titled “9.1 POST /internal/companies/{companyId}/basic”Purpose
Section titled “Purpose”Create or update Basic subscription state for a company.
Internal auth required.
Who can call
Section titled “Who can call”- Platform Admin Backend
- optional billing/reconciliation jobs
Path params
Section titled “Path params”companyId— UUID
Request body
Section titled “Request body”{ "status": "active", "startsAt": "2026-04-16T00:00:00Z", "endsAt": "2026-05-16T00:00:00Z", "source": "platform_admin", "externalReference": "sub_123"}Valid status values
Section titled “Valid status values”activeinactivecancelledexpiredtrialpaused
Behavior
Section titled “Behavior”- resolve package
basic - upsert
company_subscriptions - bump
entitlementVersion - write entitlement history
- return normalized summary
Success — 200
Section titled “Success — 200”{ "success": true, "data": { "companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "hasBasic": true, "basePackage": "basic", "entitlementVersion": 8 }}Example deactivation request
Section titled “Example deactivation request”{ "status": "inactive", "source": "platform_admin"}9.2 POST /internal/companies/{companyId}/addons
Section titled “9.2 POST /internal/companies/{companyId}/addons”Purpose
Section titled “Purpose”Create or update add-on state for one company/add-on pair.
Internal auth required.
Who can call
Section titled “Who can call”- Platform Admin Backend
- optional billing/reconciliation jobs
Path params
Section titled “Path params”companyId— UUID
Request body
Section titled “Request body”{ "addonKey": "finance", "status": "active", "startsAt": "2026-04-16T00:00:00Z", "endsAt": "2026-05-16T00:00:00Z", "source": "platform_admin", "externalReference": "addon_sub_123"}Behavior
Section titled “Behavior”- resolve addon by
addonKey - upsert
company_addons - bump
entitlementVersion - write entitlement history
- return normalized addon result
Success — 200
Section titled “Success — 200”{ "success": true, "data": { "companyId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "addonKey": "finance", "status": "active", "entitlementVersion": 9 }}Example deactivate addon
Section titled “Example deactivate addon”{ "addonKey": "finance", "status": "inactive", "source": "platform_admin"}Validation errors
Section titled “Validation errors”Missing addonKey
Section titled “Missing addonKey”{ "success": false, "error": { "code": "validation_error", "message": "addonKey is required" }}Unknown addonKey
Section titled “Unknown addonKey”{ "success": false, "error": { "code": "not_found", "message": "addon not found" }}10. Versioning and invalidation contract
Section titled “10. Versioning and invalidation contract”10.1 Version source
Section titled “10.1 Version source”Core returns entitlementVersion in all company entitlement responses.
10.2 Mandatory version bumps
Section titled “10.2 Mandatory version bumps”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
10.3 Contract expectation for Auth
Section titled “10.3 Contract expectation for Auth”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.
11. Request/response behavior rules
Section titled “11. Request/response behavior rules”11.1 Time format
Section titled “11.1 Time format”All timestamps are ISO-8601 UTC strings, example:
2026-04-16T05:00:00Z11.2 Nullability
Section titled “11.2 Nullability”basePackagemay benullstartsAt/endsAtmay benull- arrays should be empty arrays, not null, unless there is a strong reason otherwise
11.3 Normalization
Section titled “11.3 Normalization”Responses must be normalized and not leak DB implementation details.
{ "addons": [ { "key": "finance", "status": "active" } ]}{ "addons": [ { "addon_id": "uuid", "company_addon_status": "A" } ]}12. HTTP status map
Section titled “12. HTTP status map”| Status | Meaning |
|---|---|
| 200 | Successful read/update |
| 201 | Successful creation |
| 400 | Validation error |
| 401 | Missing/invalid internal auth |
| 403 | Authenticated internal caller but not allowed |
| 404 | Resource not found |
| 409 | Conflict |
| 500 | Internal server error |
| 503 | Dependency not ready / service unavailable |
13. Typical integration flows
Section titled “13. Typical integration flows”13.1 Auth access aggregation flow
Section titled “13.1 Auth access aggregation flow”- Frontend calls
/auth/me/access - Auth loads user grants from Auth DB
- Auth calls:
GET /internal/companies/{companyId}/entitlements
- Core returns normalized company entitlements
- Auth merges:
- entitlements ∩ membership grants
- Auth returns effective access to frontend
13.2 Platform Admin catalog flow
Section titled “13.2 Platform Admin catalog flow”- Platform Admin UI calls Admin Backend
- Admin Backend validates platform-admin access
- Admin Backend calls Core internal catalog write endpoint
- Core writes DB rows and mapping rows
- Core returns normalized object
13.3 Platform Admin subscription flow
Section titled “13.3 Platform Admin subscription flow”- Platform Admin UI calls Admin Backend
- Admin Backend validates admin rights
- Admin Backend calls
POST /internal/companies/{companyId}/basicor/addons - Core updates state
- Core bumps version
- Auth cache becomes stale and must rebuild on next access resolution
14. Contract notes for QA
Section titled “14. Contract notes for QA”QA should validate at minimum:
GET /healthGET /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
15. Final summary
Section titled “15. Final summary”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.
Level 3 — Permissions (OWNED BY AUTH)
Section titled “Level 3 — Permissions (OWNED BY AUTH)”Defines actions inside modules. Core MUST NOT store or evaluate this.
Critical rule
Section titled “Critical rule”Core returns entitlements ONLYCore never computes user access17. Relationship with Auth (STRICT)
Section titled “17. Relationship with Auth (STRICT)”Platform Core is a dependency of Auth.
Auth → Core → Auth → FrontendResponsibilities
Section titled “Responsibilities”Core:
- returns company entitlements
- returns enabled modules
- returns entitlementVersion
Auth:
- merges entitlements with user grants
- computes effective access
- returns final access model
Forbidden usage
Section titled “Forbidden usage”Core must NEVER:
- return user-level access
- validate permissions
- validate roles
- decide if a request is allowed
18. Effective access boundary
Section titled “18. Effective access boundary”Core MUST NOT implement:
effective_access = entitlements ∩ grantsThis 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.
20. Frontend & Backend interaction rules
Section titled “20. Frontend & Backend interaction rules”Frontend
Section titled “Frontend”Frontend must NOT call Platform Core directly for access.
Allowed frontend calls:
- Auth
- Base backend
- module backends
- admin backend
Business backends
Section titled “Business backends”Business backends must NOT:
- call Core for user access decisions
- use Core as authorization service
They must rely on:
Auth → /auth/me/access21. Final enforcement rule
Section titled “21. Final enforcement rule”Core = commercial truthAuth = access truthBackend = enforcementFrontend = UX only