Backend Admin API Contract
Related documentation: Backend Admin · Backend Core · Backend Core API · Backend Auth · Admin Platform Spec
Detailed Target Contract for Platform Admin API
Section titled “Detailed Target Contract for Platform Admin API”Audience: backend engineers, frontend engineers, QA, product, operations
Status: docs-first target contract with runtime notes
Scope: this page describes the intended Admin API surface for the new system. It is more important than the legacy runtime shape when the two differ, but implemented runtime route families are called out where they already exist.
Important reading rule
Section titled “Important reading rule”This page is the target Admin contract.
- some of these endpoints do not exist yet in the current
Backend-Kisum-Admin - some currently existing Admin endpoints are legacy and are not the final shape
- where an endpoint is described below, its job is to provide the staff-facing control-plane surface, while Core or Auth remains the source of truth underneath
- current runtime exception:
Backend-Kisum-Adminnow also proxies Market directory routes under/api/v1/admin/artists-directory/*
Base rules
Section titled “Base rules”- Base path:
/api/v1/admin - Auth:
Authorization: Bearer <access_token> - Caller must be platform staff
- Admin orchestrates
- Core stores company/commercial truth
- Auth stores identity/access truth
- Market stores artists-directory truth for artists, market companies, people, taxonomy, and related directory relations
Namespace boundary
Section titled “Namespace boundary”/api/v1/admin/companiesis reserved for Core tenant-company control-plane operations./api/v1/admin/artists-directory/companiesis the Market directory-company namespace.- The same split applies conceptually to related people/contact directory endpoints.
1. Common response and request rules
Section titled “1. Common response and request rules”1.1 Request headers
Section titled “1.1 Request headers”All protected requests:
Authorization: Bearer <access_token>Content-Type: application/json1.2 Response shape
Section titled “1.2 Response shape”Recommended Admin response envelope:
{ "success": true, "data": {}}For lists:
{ "success": true, "data": { "items": [], "total": 0, "page": 1, "limit": 20 }}For errors:
{ "success": false, "error": { "code": "validation_error", "message": "Human-readable error message" }}1.3 Admin orchestration rule
Section titled “1.3 Admin orchestration rule”Every endpoint below should document three things:
- what the endpoint does for platform staff
- what backend truth it writes to
- what downstream effect should happen
2. Packages
Section titled “2. Packages”Packages are commercial catalog entities. Admin provides the staff-facing management surface, but Core stores the truth.
2.1 GET /api/v1/admin/packages
Section titled “2.1 GET /api/v1/admin/packages”Purpose
List packages for platform administrators.
Target
- read from Core package catalog
- display package metadata and mapped modules
Request body
- no body
Response shape
Admin returns the Core subscription-summary payload unchanged.
The response contains:
hasBasicbasePackageitemsentitlementVersion
items contains one row per active commercial assignment:
- one
packagerow when a base package is active - one
addonrow per active add-on - one
restrictionrow per active restriction package (negative org override)
Each item includes:
idkeynamedescriptionstatusstartsAtendsAtpriceMinorcurrencybillingIntervaltaxCodetaxInclusivetrialDaysregionPricing
Important:
priceMinoris stored as a decimal amount- example:
199.00 USDis sent/stored as199.00
Example:
{ "success": true, "data": { "companyId": "fb8163f2-587f-4c7b-b5bc-628e7b121520", "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": [], "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" } ], "entitlementVersion": 7 }}2.2 POST /api/v1/admin/packages
Section titled “2.2 POST /api/v1/admin/packages”Purpose
Create a new commercial package from the Admin control plane.
Target
- create package in Core
- optionally map modules during creation
Body
{ "key": "basic", "name": "Basic", "description": "Basic subscription that enables the core application", "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"]}Field meaning
key- stable package identifier used across systems
name- display name shown in admin UI and potentially billing flows
description- internal or UI-facing commercial description
isActive- whether the package can be assigned to companies
priceMinor- commercial price as a decimal amount, for example
199.00
- commercial price as a decimal amount, for example
currency- 3-letter currency code
billingInterval- one of
monthly,quarterly,yearly,one_time
- one of
taxCode- optional tax classification
taxInclusive- whether the stored price already includes tax
trialDays- free-trial duration in days
regionPricing- optional region-specific price overrides
moduleKeys- modules bundled in the package
2.3 GET /api/v1/admin/packages/{packageId}
Section titled “2.3 GET /api/v1/admin/packages/{packageId}”Purpose
Read one package in detail.
Target
- fetch package metadata and its module mappings from Core
Request body
- no body
2.4 PATCH /api/v1/admin/packages/{packageId}
Section titled “2.4 PATCH /api/v1/admin/packages/{packageId}”Purpose
Update a package without recreating it.
Target
- patch package metadata in Core
- optionally replace mapped modules
Body
{ "name": "Basic Plan", "description": "Updated basic plan description", "isActive": true, "priceMinor": 119.00, "currency": "USD", "billingInterval": "yearly", "trialDays": 30, "moduleKeys": ["basic", "finance"]}Important rule
- if
moduleKeysis omitted, package-module mappings should remain unchanged - if
moduleKeysis provided, it should replace the package’s module mapping set
2.5 PUT /api/v1/admin/packages/{packageId}/modules
Section titled “2.5 PUT /api/v1/admin/packages/{packageId}/modules”Purpose
Replace the module mapping for a package explicitly.
Target
- update package-module relationships in Core
Body
{ "moduleKeys": ["basic", "finance", "venue"]}2.6 DELETE /api/v1/admin/packages/{packageId}
Section titled “2.6 DELETE /api/v1/admin/packages/{packageId}”Purpose
Delete a package from the platform catalog.
Target
- delete package in Core
Important rule
- delete must fail when the package is already assigned to companies
3. Modules
Section titled “3. Modules”Modules represent commercial features/products in the platform catalog.
3.1 GET /api/v1/admin/modules
Section titled “3.1 GET /api/v1/admin/modules”Purpose
List the module catalog.
Target
- read module definitions from Core
Request body
- no body
3.2 POST /api/v1/admin/modules
Section titled “3.2 POST /api/v1/admin/modules”Purpose
Create a new module in the commercial catalog.
Target
- create module in Core
Body
{ "key": "finance", "name": "Finance", "type": "addon", "description": "Finance module", "isActive": true}Field meaning
key- stable identifier
name- display name
type- commercial/module grouping type
description- human-readable explanation of the module
isActive- whether the module may be used in active catalog configurations
3.3 GET /api/v1/admin/modules/{moduleId}
Section titled “3.3 GET /api/v1/admin/modules/{moduleId}”Purpose
Get one module definition.
Request body
- no body
3.4 PATCH /api/v1/admin/modules/{moduleId}
Section titled “3.4 PATCH /api/v1/admin/modules/{moduleId}”Purpose
Update module metadata or lifecycle state.
Target
- patch module metadata in Core
Body
{ "name": "Finance Module", "description": "Updated finance module description", "isActive": true}3.5 DELETE /api/v1/admin/modules/{moduleId}
Section titled “3.5 DELETE /api/v1/admin/modules/{moduleId}”Purpose
Delete a module from the commercial catalog.
Target
- delete module in Core
Important rule
- delete must fail when the module is still linked to packages or add-ons
4. Add-ons
Section titled “4. Add-ons”Add-ons are commercial catalog entities sold or assigned on top of packages.
4.1 GET /api/v1/admin/addons
Section titled “4.1 GET /api/v1/admin/addons”Purpose
List add-ons available in the platform catalog.
Request body
- no body
4.2 POST /api/v1/admin/addons
Section titled “4.2 POST /api/v1/admin/addons”Purpose
Create an add-on.
Target
- create add-on in Core
- optionally map modules during creation
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"]}Important rule
- add-ons may only map modules whose module
typeisaddon
4.3 GET /api/v1/admin/addons/{addonId}
Section titled “4.3 GET /api/v1/admin/addons/{addonId}”Purpose
Get one add-on in detail.
Request body
- no body
4.4 PATCH /api/v1/admin/addons/{addonId}
Section titled “4.4 PATCH /api/v1/admin/addons/{addonId}”Purpose
Update add-on metadata and optionally its module mappings.
Body
{ "name": "Finance Add-on", "description": "Updated finance add-on description", "isActive": true, "priceMinor": 59.00, "currency": "USD", "billingInterval": "yearly", "moduleKeys": ["finance"]}4.5 PUT /api/v1/admin/addons/{addonId}/modules
Section titled “4.5 PUT /api/v1/admin/addons/{addonId}/modules”Purpose
Replace the mapped modules for an add-on.
Body
{ "moduleKeys": ["finance", "market"]}4.6 DELETE /api/v1/admin/addons/{addonId}
Section titled “4.6 DELETE /api/v1/admin/addons/{addonId}”Purpose
Delete an add-on from the platform catalog.
Target
- delete add-on in Core
Important rule
- delete must fail when the add-on is already assigned to companies
4A. Restriction packages
Section titled “4A. Restriction packages”Restriction packages are negative catalog entries: they list Auth permission keys to deny for a company when attached. They are managed under Packages & Add-ons → Restrictions in Admin FE.
Admin proxies Core catalog CRUD under /api/v1/admin/restriction-packages (same auth as add-ons catalog).
4A.1 GET /api/v1/admin/restriction-packages
Section titled “4A.1 GET /api/v1/admin/restriction-packages”List restriction packages in the platform catalog.
4A.2 POST /api/v1/admin/restriction-packages
Section titled “4A.2 POST /api/v1/admin/restriction-packages”Create a restriction package in Core.
Body
{ "key": "agencies_contact", "name": "Agencies & artist contacts", "description": "Blocks agency directory and artist team/contact surfaces", "isActive": true, "permissionKeys": ["promoter.booking.agencies.view"]}4A.3 GET /api/v1/admin/restriction-packages/{restrictionPackageId}
Section titled “4A.3 GET /api/v1/admin/restriction-packages/{restrictionPackageId}”Get one restriction package (includes permissionKeys).
4A.4 PATCH /api/v1/admin/restriction-packages/{restrictionPackageId}
Section titled “4A.4 PATCH /api/v1/admin/restriction-packages/{restrictionPackageId}”Update name, description, active flag, or permissionKeys.
4A.5 DELETE /api/v1/admin/restriction-packages/{restrictionPackageId}
Section titled “4A.5 DELETE /api/v1/admin/restriction-packages/{restrictionPackageId}”Delete from catalog. Fails when still assigned to companies.
5. Companies
Section titled “5. Companies”Company operations are initiated by Admin but stored in Core.
5.1 GET /api/v1/admin/companies
Section titled “5.1 GET /api/v1/admin/companies”Purpose
List companies from the platform control plane.
Target
- list company records from Core
- may aggregate profile and address summaries for UI convenience
Request body
- no body
5.2 POST /api/v1/admin/companies
Section titled “5.2 POST /api/v1/admin/companies”Purpose
Create a company through the Admin control plane.
Target
- write company master/profile/address/social/document structure into Core
Body
{ "legalName": "Kisum Entertainment Group Pte Ltd", "displayName": "Kisum Entertainment Group", "status": "active", "createdSource": "admin", "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 } } ]}Target system
Core
Important rule
- Admin should not persist this company locally as source of truth
5.3 GET /api/v1/admin/companies/{companyId}
Section titled “5.3 GET /api/v1/admin/companies/{companyId}”Purpose
Get one company in detail.
Target
- read company bundle from Core
Request body
- no body
5.4 PATCH /api/v1/admin/companies/{companyId}
Section titled “5.4 PATCH /api/v1/admin/companies/{companyId}”Purpose
Update a company from Admin.
Target
- patch company master and optional nested sections in Core
Body
{ "displayName": "Kisum Entertainment Group", "status": "active", "metadata": { "region": "SG", "tier": "enterprise" }, "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": "0f4de9f7-6f87-4f8c-8209-7480ac8d2374", "type": "primary", "line1": "120 Orchard Road", "line2": "Level 15", "city": "Singapore", "state": "Singapore", "postalCode": "238888", "country": "Singapore", "isPrimary": true } ], "socialLinks": [ { "id": "2c2f57c9-cce0-4a6e-8dc9-cf2d2e4e86c7", "platform": "instagram", "label": "Official Instagram", "url": "https://instagram.com/kisumgroup" } ], "documents": [ { "id": "698c5ef5-1c93-4d8f-b644-80d7d3a75af0", "type": "business_registration", "name": "Updated Business Registration Certificate", "storageKey": "companies/company-id/business-registration-v2.pdf", "url": "https://files.kisum.dev/companies/company-id/business-registration-v2.pdf", "mimeType": "application/pdf", "sizeBytes": 251004, "metadata": { "version": 2 } } ]}Important partial-update rule
- omitted collection keys mean no change
- empty collection arrays mean replace with empty set
5.5 POST /api/v1/admin/companies/{companyId}/approve
Section titled “5.5 POST /api/v1/admin/companies/{companyId}/approve”Purpose
Approve a draft company from the admin review workflow. Approval activates the company — there is no separate approve-then-activate step for signup drafts. For test_submission records, the same endpoint also repairs an older partially provisioned active company.
Orchestration (implemented)
- Load company state from Core (
GET /internal/companies/{id}). Acceptdraft, plusactiveonly fortest_submissionreconciliation. - Resolve Auth memberships. If the signup membership is missing, recover the applicant from
metadata.signupUserIdand recreate theTENANT_SUPERADMINmembership. - Approve/activate
PENDINGusers, acceptAPPROVEDusers on retries, and activate membership rows. Rejected/deleted users fail closed. - Test-submission signups: Core
POST /internal/companies/{id}/activate-signupprovisions or repairs the package/add-ons from metadata, then Admin reloads memberships and replaces owner/tenant-superadmin permission grants from enabled modules. - Other draft companies: Core
PATCH /internal/companies/{id}with{ "status": "active" }. - Return refreshed Core company bundle.
Body
Optional JSON (currently ignored); {} is fine.
5.6 POST /api/v1/admin/companies/{companyId}/reject
Section titled “5.6 POST /api/v1/admin/companies/{companyId}/reject”Purpose
Reject a draft company signup.
Orchestration (implemented)
- Require
status = draft(idempotent when alreadyarchived). - Reject pending Auth users linked via company memberships (
approvalStatus = REJECTED,isActive = false). - Core
PATCH /internal/companies/{id}with{ "status": "archived" }. - Return refreshed Core company bundle.
Body
Optional JSON (currently ignored).
5.7 POST /api/v1/admin/companies/{companyId}/activate
Section titled “5.7 POST /api/v1/admin/companies/{companyId}/activate”Purpose
Re-activate a company that was deactivated (inactive or suspended) or archived (e.g. after Reject). Not used for draft signup approval — use Approve instead. The platform tombstone company (00000000-0000-0000-0000-000000000001) cannot be activated.
Orchestration (implemented)
inactive/suspended/archived→ CorePATCH{ "status": "active" }.draft→409(use Approve for draft signup companies).- Already
active→ idempotent; returns current bundle.
Body
Optional JSON (currently ignored).
5.8 POST /api/v1/admin/companies/{companyId}/deactivate
Section titled “5.8 POST /api/v1/admin/companies/{companyId}/deactivate”Purpose
Deactivate an active company (active → inactive).
Orchestration (implemented)
active→ CorePATCH{ "status": "inactive" }.- Already
inactive→ idempotent. - Other statuses →
409 conflict.
Body
Optional JSON (currently ignored).
Admin UI button rules (Frontend-Kisum-Admin company detail → Overview)
| Status | Approve | Reject | Activate | Deactivate | Hard delete |
|---|---|---|---|---|---|
draft | enabled | enabled | disabled | disabled | disabled |
active | disabled | disabled | disabled | enabled | disabled |
inactive | disabled | disabled | enabled | disabled | enabled |
archived (rejected) | disabled | disabled | enabled | disabled | enabled |
deleting | disabled | disabled | disabled | disabled | enabled |
suspended | disabled | disabled | enabled | disabled | disabled |
5.9 Company hard-delete (async purge)
Section titled “5.9 Company hard-delete (async purge)”Platform staff hard-delete orchestration. Full matrix, tombstone rules, and ops runbook: Company hard-delete lifecycle.
GET /api/v1/admin/companies/{companyId}/delete-preview
Section titled “GET /api/v1/admin/companies/{companyId}/delete-preview”Aggregated per-service row counts and blockers before confirm. Proxies Core + Auth + Promoters + Finance + Artists + Venues + Checkout machine previews.
POST /api/v1/admin/companies/{companyId}/delete
Section titled “POST /api/v1/admin/companies/{companyId}/delete”Starts async purge. Body: { "force": boolean, "reason": string, "confirmName": string } — confirmName must match Core legal or display name. Creates Core company_delete_jobs row (locks company status=deleting), returns { "jobId": "…" }. Admin orchestrator runs purge steps asynchronously.
GET /api/v1/admin/companies/{companyId}/delete-status/{jobId}
Section titled “GET /api/v1/admin/companies/{companyId}/delete-status/{jobId}”Poll job status, per-step progress (Admin overlay + Core job row), and errorMessage.
Breaking: legacy DELETE /api/v1/admin/companies/{companyId} returns 405 — use POST …/delete.
6. Company commercial management
Section titled “6. Company commercial management”These endpoints are staff-facing wrappers around Core commercial operations.
6.1 GET /api/v1/admin/companies/{companyId}/subscription
Section titled “6.1 GET /api/v1/admin/companies/{companyId}/subscription”Purpose
Read the current company subscription summary.
Target
- read from Core subscription-summary view
Request body
- no body
6.2 POST /api/v1/admin/companies/{companyId}/package
Section titled “6.2 POST /api/v1/admin/companies/{companyId}/package”Purpose
Assign or change the base package for a company.
Target
- write to Core package/subscription assignment flow
Body
{ "packageKey": "basic", "status": "active", "startsAt": "2026-04-19T00:00:00Z", "endsAt": "2026-05-19T00:00:00Z", "source": "platform_admin", "externalReference": "stripe_sub_123"}6.3 POST /api/v1/admin/companies/{companyId}/addons
Section titled “6.3 POST /api/v1/admin/companies/{companyId}/addons”Purpose
Add an add-on to a company.
Target
- write to Core company add-on assignment flow
Body
{ "addonKey": "finance", "status": "active", "startsAt": "2026-04-19T00:00:00Z", "endsAt": "2026-05-19T00:00:00Z", "source": "platform_admin", "externalReference": "stripe_addon_finance_123"}Admin also accepts:
{ "addonId": "b8c37dfb-d154-408a-a7e8-fbfab9ff6a2c", "status": "active"}When addonId is sent, Admin resolves the catalog add-on id to the Core addonKey before calling Core.
6.4 DELETE /api/v1/admin/companies/{companyId}/addons/{addonId}
Section titled “6.4 DELETE /api/v1/admin/companies/{companyId}/addons/{addonId}”Purpose
Remove an add-on from a company.
Request body
- no body
Important
{addonId}must be the catalog add-on id from the subscriptionitems[]row- Admin resolves that id to the Core
addonKeybefore deactivating the company add-on
6.4A POST /api/v1/admin/companies/{companyId}/restrictions
Section titled “6.4A POST /api/v1/admin/companies/{companyId}/restrictions”Attach or update a restriction package on a company.
Body
{ "restrictionKey": "agencies_contact", "status": "active"}Admin also accepts restrictionId (catalog UUID from subscription items[]) and resolves it to restrictionKey before calling Core.
6.4B DELETE /api/v1/admin/companies/{companyId}/restrictions/{restrictionId}
Section titled “6.4B DELETE /api/v1/admin/companies/{companyId}/restrictions/{restrictionId}”Deactivate a company restriction. {restrictionId} is the catalog restriction package id from subscription items[] where kind is restriction.
6.5 GET /api/v1/admin/companies/{companyId}/entitlements
Section titled “6.5 GET /api/v1/admin/companies/{companyId}/entitlements”Purpose
Inspect the resulting company entitlements.
Target
- read entitlement result from Core
Request body
- no body
6.6 GET /api/v1/admin/companies/{companyId}/history
Section titled “6.6 GET /api/v1/admin/companies/{companyId}/history”Purpose
Inspect commercial history for audit, support, or troubleshooting.
Target
- read from Core entitlement/subscription history
Request body
- no body
7. Delegation rules and access governance
Section titled “7. Delegation rules and access governance”These endpoints are staff-facing wrappers around Auth-side access governance.
7.1 GET /api/v1/admin/delegation-rules
Section titled “7.1 GET /api/v1/admin/delegation-rules”Purpose
List the current delegation rules used by Auth.
Target
- read from Auth delegation policy state
Request body
- no body
7.2 POST /api/v1/admin/delegation-rules
Section titled “7.2 POST /api/v1/admin/delegation-rules”Purpose
Create a new delegation rule set.
Target
- write delegation policy into Auth
Body
{ "role": "ADMIN", "grantableModules": ["basic", "finance"], "grantablePermissions": [ "finance.read", "finance.write", "users.manage" ], "canManageUsers": true, "canBuyAddons": false}7.3 PATCH /api/v1/admin/delegation-rules/{ruleId}
Section titled “7.3 PATCH /api/v1/admin/delegation-rules/{ruleId}”Purpose
Update delegation limits.
Target
- patch Auth-side delegation policy
Body
{ "grantableModules": ["basic"], "grantablePermissions": [ "finance.read" ], "canManageUsers": false, "canBuyAddons": false}8. Platform staff, users, and memberships
Section titled “8. Platform staff, users, and memberships”These routes should exist in Admin as the platform-staff control surface, even when the underlying source of truth is Auth.
8.0 Users vs memberships
Section titled “8.0 Users vs memberships”This distinction must be understood before implementing any Admin user-access screen.
A user is an identity
Section titled “A user is an identity”A user is the person/account record stored in Auth.
It answers identity-level questions such as:
- who is this person
- what is their email
- what is their full name
- are they active
- do they have a platform global role
- what sessions do they currently have
Typical user fields:
idemailfullNameisActiveglobalRoleapprovalStatus
A user exists independently of any company or business unit.
One user can exist even if they are not assigned to any tenant scope yet.
A membership is a scoped assignment
Section titled “A membership is a scoped assignment”A membership is the relationship between a user and an organizational scope.
That scope can be:
- a company
- a business unit
A membership answers access-level questions such as:
- is this user assigned to this company
- is this user assigned to this business unit
- what role do they have in that scope
- is that scoped assignment active
- which module grants apply in that scope
- which permission grants apply in that scope
- which delegation limits apply in that scope
Typical membership fields:
userIdcompanyId- optional
businessUnitId roleisActivemoduleGrantspermissionGrantsdelegation
One user can have many memberships
Section titled “One user can have many memberships”This is the core reason the two endpoint groups are different.
Example:
User- id: U1- email: marco@kisum.io
Company membership- user_id: U1- company_id: C1- role: ADMIN
Business-unit membership- user_id: U1- company_id: C1- business_unit_id: BU7- role: APPROVER
Business-unit membership- user_id: U1- company_id: C1- business_unit_id: BU9- role: MEMBERThis is still one user, but multiple scoped memberships.
When to use /users
Section titled “When to use /users”Use users endpoints when the screen is identity-centered.
That means the main question is:
- who are the people/accounts in the system
Use users endpoints for:
- global user directories
- platform-user directories
- user detail pages
- user activation/deactivation
- user context inspection
- session inspection
Examples:
GET /api/v1/admin/platform-usersGET /api/v1/admin/usersGET /api/v1/admin/platform-users/{userId}GET /api/v1/admin/platform-users/{userId}/context
When to use /memberships
Section titled “When to use /memberships”Use memberships endpoints when the screen is scope-assignment-centered.
That means the main question is:
- who belongs to this company or business unit, and with what scoped role/access
Use memberships endpoints for:
- company access administration
- business-unit access administration
- role assignment in tenant scopes
- module grant editing
- permission grant editing
- delegation configuration in tenant scopes
Examples:
GET /api/v1/admin/companies/{companyId}/membershipsPOST /api/v1/admin/companies/{companyId}/membershipsDELETE /api/v1/admin/companies/{companyId}/memberships/{membershipId}
GET …/memberships accepts ?includeInactive=true to also return deactivated memberships (is_active = false), so the memberships screen can show and then delete them; the default and the /users directory list stay active-only.
GET /api/v1/admin/companies/{companyId}/business-units/{businessUnitId}/membershipsPOST /api/v1/admin/companies/{companyId}/business-units/{businessUnitId}/memberships
Deactivate vs. hard-delete. POST …/memberships with isActive: false deactivates a membership (the row stays; access is revoked). DELETE …/memberships/{membershipId} permanently removes it — the company membership, the user’s business-unit memberships in that company, and all module/permission rules, grants, and delegations for that company, in one transaction. The user account and the user’s memberships in other companies are untouched. Admin proxies to Auth’s machine route; Auth refuses to remove a company’s last tenant super admin (409 last_tenant_admin) and writes an audit log. Prefer delete over an accumulating pile of deactivated rows — a deactivated membership that downstream code forgets to filter reappears where it should not (it surfaced in the Promoters company switcher).
Why both /users and /memberships may appear in one screen
Section titled “Why both /users and /memberships may appear in one screen”Some Admin screens need both views at the same time.
Example:
- a company page may show
users- human directory view
- useful for search, contact, status, and profile inspection
- the same company page may also show
memberships- access administration view
- useful for role changes, grants, and delegation settings
So:
users= identity-oriented representationmemberships= access-oriented representation
Short rule
Section titled “Short rule”Use this rule during implementation:
user= who the person/account ismembership= what access assignment that person has in a specific scope
8.1 GET /api/v1/admin/platform-users
Section titled “8.1 GET /api/v1/admin/platform-users”Purpose
List users visible to platform staff.
Target
- read through Auth user-administration APIs
Request body
- no body
Typical query parameters
approvalStatusglobalRolelimitoffset
Admin backend mapping
- Admin endpoint:
GET /api/v1/admin/platform-users - Auth upstream:
GET /internal/users?globalRole=true - Auth mode: forwarded
Authorization: Bearer <token>
This route is for the platform-staff directory only.
8.1A GET /api/v1/admin/users
Section titled “8.1A GET /api/v1/admin/users”Purpose
List all non-platform users across the system, not filtered by company.
Target
- read tenant/non-platform users through Auth
Request body
- no body
Typical query parameters
approvalStatuslimitoffset
Admin backend mapping
- Admin endpoint:
GET /api/v1/admin/users - Auth upstream:
GET /internal/users?globalRole=false - Auth mode: forwarded
Authorization: Bearer <token>
Important rule
- this is the global tenant-user directory
- this is not limited to one company
- company-specific user views must use the company-scoped routes below
8.1B Test-submission signup requests (2026-07-08)
Section titled “8.1B Test-submission signup requests (2026-07-08)”Purpose
Orchestrate approval/rejection for marketing test submission signups. Approval accepts a draft company or retries an active partial approval; other company states fail closed.
| Method | Admin route | Upstream |
|---|---|---|
| POST | /api/v1/admin/signup-requests/{userId}/approve | Retry-safe: Auth user PENDING or APPROVED; membership activate; Core activate-signup; owner permission grants reseeded |
| POST | /api/v1/admin/signup-requests/{userId}/reject | Auth REJECTED + isActive=false; Core PATCH /internal/companies/{id} status=archived when draft test submission |
| PATCH | /api/v1/admin/users/{id} | Alias to PATCH /api/v1/admin/platform-users/{id} (Auth bearer user patch) |
Frontend: Frontend-Kisum-Admin → /users list (approvalStatus filter + column) and user detail approve/reject actions.
8.2 POST /api/v1/admin/platform-users
Section titled “8.2 POST /api/v1/admin/platform-users”Purpose
Create a user from Admin.
Target
- create user in Auth
Body
{ "email": "ops-admin@kisum.dev", "fullName": "Ops Admin", "password": "TemporaryPassword123!", "globalRole": "PLATFORM_ADMIN", "approvalStatus": "APPROVED", "isActive": true}Admin backend mapping
- Admin endpoint:
POST /api/v1/admin/platform-users - Auth upstream:
POST /internal/users - Auth mode: forwarded
Authorization: Bearer <token>
8.3 GET /api/v1/admin/platform-users/{userId}
Section titled “8.3 GET /api/v1/admin/platform-users/{userId}”Purpose
Get one user from the platform-admin perspective.
Target
- read user detail from Auth
Request body
- no body
Admin backend mapping
- Admin endpoint:
GET /api/v1/admin/platform-users/{userId} - Auth upstream:
GET /internal/users/{id} - Auth mode: forwarded
Authorization: Bearer <token>
8.4 PATCH /api/v1/admin/platform-users/{userId}
Section titled “8.4 PATCH /api/v1/admin/platform-users/{userId}”Purpose
Update a user from Admin.
Target
- patch user in Auth
Body
{ "fullName": "Updated Ops Admin", "globalRole": "PLATFORM_SUPERADMIN", "approvalStatus": "APPROVED", "isActive": true}Admin backend mapping
- Admin endpoint:
PATCH /api/v1/admin/platform-users/{userId} - Auth upstream:
PATCH /internal/users/{id} - Auth mode: forwarded
Authorization: Bearer <token>
8.5 DELETE /api/v1/admin/platform-users/{userId}
Section titled “8.5 DELETE /api/v1/admin/platform-users/{userId}”Purpose
Deactivate a user from Admin.
Target
- deactivate user in Auth
Request body
- no body
Admin backend mapping
- Admin endpoint:
DELETE /api/v1/admin/platform-users/{userId} - Auth upstream:
DELETE /internal/users/{id} - Auth mode: forwarded
Authorization: Bearer <token>
8.6 GET /api/v1/admin/platform-users/{userId}/context
Section titled “8.6 GET /api/v1/admin/platform-users/{userId}/context”Purpose
Read the full admin-visible user context, including memberships.
Target
- read Auth user-context output
Request body
- no body
Admin backend mapping
- Admin endpoint:
GET /api/v1/admin/platform-users/{userId}/context - Auth machine endpoint:
GET /internal/admin/users/{id}/context - Auth mode:
X-Internal-API-KeyusingAUTH_INTERNAL_API_KEY
8.7 POST /api/v1/admin/platform-users/{userId}/revoke-all
Section titled “8.7 POST /api/v1/admin/platform-users/{userId}/revoke-all”Purpose
Revoke all sessions for a user.
Target
- revoke sessions in Auth
Body
{}Admin backend mapping
- Admin endpoint:
POST /api/v1/admin/platform-users/{userId}/revoke-all - Auth machine endpoint:
POST /internal/admin/users/{id}/revoke-all - Auth mode:
X-Internal-API-KeyusingAUTH_INTERNAL_API_KEY
8.8 POST /api/v1/admin/platform-users/invitations
Section titled “8.8 POST /api/v1/admin/platform-users/invitations”Purpose
Invite a new platform staff user.
Target
- create invitation in Auth
- optionally trigger invitation email flow
Body
{ "email": "ops-admin@kisum.dev", "globalRole": "PLATFORM_ADMIN", "name": "Ops Admin"}Current implementation note
Platform-user invitations still require a dedicated Auth-backed implementation contract. If that upstream invitation path is not present in the running Auth deployment, Admin must treat this action as unavailable rather than inventing a separate endpoint.
8.8A GET /api/v1/admin/sessions
Section titled “8.8A GET /api/v1/admin/sessions”Purpose
List active sessions across the platform-admin scope.
Target
- read active-session data from Auth
- support “who is currently connected” views in Admin
- support operational filtering by user, company, approval status, and platform-vs-tenant scope
Request body
- no body
Typical query parameters
approvalStatusglobalRoleuserIdcompanyIdlimitoffset
Query behavior
approvalStatus- optional
- when omitted, no approval-status filtering is applied
- when provided, must match the Auth approval-status enum exactly
globalRole- optional
truemeans platform users onlyfalsemeans tenant users only- a comma-separated role list can also be used for explicit role filtering
userId- optional UUID
- when provided, restricts the result to sessions for a single user
companyId- optional UUID
- when provided, returns sessions for users who currently have an active membership in that company
limit- default
50 - upper bounded by Auth validation
- default
offset- default
0
- default
Definition
A session is considered active when all of the following are true:
is_revoked = falseexpires_at > now()- the user row still exists
This route represents authenticated active-session presence, not websocket-style realtime presence.
Admin backend mapping
- Admin endpoint:
GET /api/v1/admin/sessions - Auth machine endpoint:
GET /internal/admin/sessions - Auth mode:
X-Internal-API-KeyusingAUTH_INTERNAL_API_KEY
Response shape
{ "success": true, "data": [ { "sessionId": "8b4d1c3a-1a9e-4e93-b71f-9d8d8d7a7e90", "userId": "9e280dc5-c1cf-4d62-bad9-0774e4a3b9e1", "email": "ops-admin@kisum.dev", "fullName": "Ops Admin", "globalRole": "PLATFORM_ADMIN", "approvalStatus": "APPROVED", "isUserActive": true, "ipAddress": "103.21.244.1", "userAgent": "Mozilla/5.0 ...", "deviceName": "Chrome on macOS", "createdAt": "2026-04-19T02:00:00Z", "lastUsedAt": "2026-04-19T08:41:00Z", "expiresAt": "2026-05-19T02:00:00Z" } ]}Operational use
This route should back:
- connected-user dashboards
- fraud or security review
- support diagnostics
- company-scoped activity checks
8.8B GET /api/v1/admin/session-stats
Section titled “8.8B GET /api/v1/admin/session-stats”Purpose
Return aggregate counts for active sessions and connected users.
Target
- power dashboard counters
- separate platform-user activity from tenant-user activity
Request body
- no body
Typical query parameters
approvalStatusglobalRoleuserIdcompanyId
These filters use the same semantics as GET /api/v1/admin/sessions, but return counts instead of individual session rows.
Admin backend mapping
- Admin endpoint:
GET /api/v1/admin/session-stats - Auth machine endpoint:
GET /internal/admin/session-stats - Auth mode:
X-Internal-API-KeyusingAUTH_INTERNAL_API_KEY
Response shape
{ "success": true, "data": { "activeSessions": 124, "connectedUsers": 87, "platformUsersConnected": 6, "tenantUsersConnected": 81 }}Meaning of each field
activeSessions- total number of active session records matching the filter
connectedUsers- distinct users with at least one active session
platformUsersConnected- distinct connected users whose
globalRoleis one of the platform roles
- distinct connected users whose
tenantUsersConnected- distinct connected users whose
globalRoleisNONE
- distinct connected users whose
8.8C GET /api/v1/admin/companies/{companyId}/sessions
Section titled “8.8C GET /api/v1/admin/companies/{companyId}/sessions”Purpose
List active sessions for users belonging to one company.
Target
- inspect company-scoped user activity
- support approval, security, and support operations for one tenant
Request body
- no body
Typical query parameters
limitoffset
Behavior
- this route is scoped by the company in the path
- Auth returns sessions for users who currently have an active membership in that company
- this is the company-scoped alternative to the global session list
Admin backend mapping
- Admin endpoint:
GET /api/v1/admin/companies/{companyId}/sessions - Auth machine endpoint:
GET /internal/admin/companies/{companyId}/sessions - Auth mode:
X-Internal-API-KeyusingAUTH_INTERNAL_API_KEY
Response shape
{ "success": true, "data": [ { "sessionId": "8b4d1c3a-1a9e-4e93-b71f-9d8d8d7a7e90", "userId": "9e280dc5-c1cf-4d62-bad9-0774e4a3b9e1", "email": "finance-user@kisum.dev", "fullName": "Finance User", "globalRole": "NONE", "approvalStatus": "APPROVED", "isUserActive": true, "ipAddress": "103.21.244.1", "userAgent": "Mozilla/5.0 ...", "deviceName": "Chrome on macOS", "createdAt": "2026-04-19T02:00:00Z", "lastUsedAt": "2026-04-19T08:41:00Z", "expiresAt": "2026-05-19T02:00:00Z" } ]}8.9 GET /api/v1/admin/companies/{companyId}/users
Section titled “8.9 GET /api/v1/admin/companies/{companyId}/users”Purpose
List company users from the platform-admin view.
Target
- read through Auth company memberships
Request body
- no body
Admin backend mapping
- Admin endpoint:
GET /api/v1/admin/companies/{companyId}/users - Auth upstream:
GET /internal/companies/{companyId}/users - Auth mode: forwarded
Authorization: Bearer <token>
8.10 POST /api/v1/admin/companies/{companyId}/memberships
Section titled “8.10 POST /api/v1/admin/companies/{companyId}/memberships”Purpose
Create or update a company membership from Admin.
Target
- write company membership in Auth
- write module grants in Auth
- write permission grants in Auth
- write delegation constraints in Auth
Body
{ "userId": "9e280dc5-c1cf-4d62-bad9-0774e4a3b9e1", "role": "ADMIN", "status": "ACTIVE", "moduleGrants": ["basic", "finance"], "permissionGrants": [ "finance.read", "finance.write", "users.manage" ], "delegation": { "grantableModules": ["basic"], "grantablePermissions": ["finance.read"], "canManageUsers": false, "canBuyAddons": false }}Admin backend mapping
- Admin endpoint:
POST /api/v1/admin/companies/{companyId}/memberships - Auth upstream:
POST /internal/companies/{companyId}/memberships - Auth mode: forwarded
Authorization: Bearer <token>
8.11 GET /api/v1/admin/companies/{companyId}/business-units
Section titled “8.11 GET /api/v1/admin/companies/{companyId}/business-units”Purpose
List Core-owned business-unit master rows for one company from the Admin control plane.
Target
- read business-unit master data from Core
- show company organizational structure owned by Core
Request body
- no body
Admin backend mapping
- Admin endpoint:
GET /api/v1/admin/companies/{companyId}/business-units - Core upstream:
GET /internal/companies/{companyId}/business-units - Core mode:
X-Internal-API-KeyusingCORE_INTERNAL_API_KEY
8.12 POST /api/v1/admin/companies/{companyId}/business-units
Section titled “8.12 POST /api/v1/admin/companies/{companyId}/business-units”Purpose
Create one Core-owned business-unit master row under a company.
Target
- write business-unit master data in Core
Body
{ "name": "Finance Operations", "code": "FINOPS", "slug": "finance-operations", "isActive": true, "metadata": { "description": "Finance team business unit" }}Admin backend mapping
- Admin endpoint:
POST /api/v1/admin/companies/{companyId}/business-units - Core upstream:
POST /internal/companies/{companyId}/business-units - Core mode:
X-Internal-API-KeyusingCORE_INTERNAL_API_KEY
8.13 GET /api/v1/admin/companies/{companyId}/business-units/{businessUnitId}
Section titled “8.13 GET /api/v1/admin/companies/{companyId}/business-units/{businessUnitId}”Purpose
Get one Core-owned business-unit master row in the scope of its parent company.
Target
- read one business-unit master row from Core
- keep business-unit reads explicitly scoped to the parent company
Request body
- no body
Admin backend mapping
- Admin endpoint:
GET /api/v1/admin/companies/{companyId}/business-units/{businessUnitId} - Core upstream target: company-scoped business-unit read
- Core mode:
X-Internal-API-KeyusingCORE_INTERNAL_API_KEY
Important rule:
- backend must verify the business unit belongs to the
{companyId}in the path
8.14 PATCH /api/v1/admin/companies/{companyId}/business-units/{businessUnitId}
Section titled “8.14 PATCH /api/v1/admin/companies/{companyId}/business-units/{businessUnitId}”Purpose
Update one Core-owned business-unit master row.
Target
- update business-unit master data in Core
Body
{ "name": "Finance & Procurement", "code": "FINPROC", "slug": "finance-procurement", "isActive": true, "metadata": { "description": "Updated business unit name" }}Admin backend mapping
- Admin endpoint:
PATCH /api/v1/admin/companies/{companyId}/business-units/{businessUnitId} - Core upstream target: company-scoped business-unit patch
- Core mode:
X-Internal-API-KeyusingCORE_INTERNAL_API_KEY
Important note:
- this route patches the Core business-unit master row
- it does not patch business-unit memberships
- memberships remain Auth-owned
- backend must verify the business unit belongs to the
{companyId}in the path
8.15 DELETE /api/v1/admin/companies/{companyId}/business-units/{businessUnitId}
Section titled “8.15 DELETE /api/v1/admin/companies/{companyId}/business-units/{businessUnitId}”Purpose
Delete or archive one Core-owned business-unit master row in the scope of its parent company.
Target
- remove a business-unit master row from Core, or mark it inactive depending on final implementation policy
Request body
- no body
Admin backend mapping
- Admin endpoint:
DELETE /api/v1/admin/companies/{companyId}/business-units/{businessUnitId} - Core upstream target: company-scoped business-unit delete
- Core mode:
X-Internal-API-KeyusingCORE_INTERNAL_API_KEY
Important rule:
- backend must verify the business unit belongs to the
{companyId}in the path - delete policy must be explicit:
- hard delete only if no dependent records exist, or
- soft delete / deactivate if business-unit history must be preserved
8.16 GET /api/v1/admin/companies/{companyId}/business-units/{businessUnitId}/users
Section titled “8.16 GET /api/v1/admin/companies/{companyId}/business-units/{businessUnitId}/users”Purpose
List users for one business unit.
Target
- read business-unit memberships from Auth
Request body
- no body
Admin backend mapping
- Admin endpoint:
GET /api/v1/admin/companies/{companyId}/business-units/{businessUnitId}/users - Auth upstream:
GET /internal/companies/{companyId}/business-units/{businessUnitId}/users - Auth mode: forwarded
Authorization: Bearer <token>
8.17 GET /api/v1/admin/companies/{companyId}/business-units/{businessUnitId}/memberships
Section titled “8.17 GET /api/v1/admin/companies/{companyId}/business-units/{businessUnitId}/memberships”Purpose
List business-unit memberships for one business unit.
Target
- read business-unit memberships from Auth
Request body
- no body
Admin backend mapping
- Admin endpoint:
GET /api/v1/admin/companies/{companyId}/business-units/{businessUnitId}/memberships - Auth upstream:
GET /internal/admin/companies/{companyId}/business-units/{businessUnitId}/memberships - Auth mode:
X-Internal-API-KeyusingAUTH_INTERNAL_API_KEY
8.18 POST /api/v1/admin/companies/{companyId}/business-units/{businessUnitId}/memberships
Section titled “8.18 POST /api/v1/admin/companies/{companyId}/business-units/{businessUnitId}/memberships”Purpose
Create or update a business-unit membership from Admin.
Target
- write business-unit membership in Auth
Body
{ "userId": "9e280dc5-c1cf-4d62-bad9-0774e4a3b9e1", "role": "MEMBER", "status": "ACTIVE"}Admin backend mapping
- Admin endpoint:
POST /api/v1/admin/companies/{companyId}/business-units/{businessUnitId}/memberships - Auth upstream:
POST /internal/companies/{companyId}/business-units/{businessUnitId}/memberships - Auth mode: forwarded
Authorization: Bearer <token>
9. Payment-provider configuration
Section titled “9. Payment-provider configuration”These are privileged operational endpoints. Their persistence details must be explicitly defined during implementation.
9.1 GET /api/v1/admin/payment-providers
Section titled “9.1 GET /api/v1/admin/payment-providers”Purpose
List payment-provider configuration visible to platform staff.
Request body
- no body
9.2 POST /api/v1/admin/payment-providers/stripe
Section titled “9.2 POST /api/v1/admin/payment-providers/stripe”Purpose
Configure Stripe for a deployment, country, or environment scope.
Body
{ "scopeType": "country", "scopeValue": "SG", "publishableKey": "pk_live_xxx", "secretReference": "secrets/stripe/sg/live", "webhookSecretReference": "secrets/stripe/sg/webhook", "isActive": true}9.3 POST /api/v1/admin/payment-providers/xendit
Section titled “9.3 POST /api/v1/admin/payment-providers/xendit”Purpose
Configure Xendit for a deployment, country, or environment scope.
Body
{ "scopeType": "country", "scopeValue": "ID", "apiKeyReference": "secrets/xendit/id/live", "callbackTokenReference": "secrets/xendit/id/callback", "isActive": true}10. Operational Admin surfaces that remain first-class
Section titled “10. Operational Admin surfaces that remain first-class”These routes are not secondary. Even in the new architecture, they remain valid Admin responsibilities and should stay fully documented.
10.1 Approvals
Section titled “10.1 Approvals”GET /api/v1/admin/approvals
Section titled “GET /api/v1/admin/approvals”Purpose
List pending approvals.
Request body
- no body
GET /api/v1/admin/approvals/{id}
Section titled “GET /api/v1/admin/approvals/{id}”Purpose
Get one approval.
Request body
- no body
POST /api/v1/admin/approvals/approve
Section titled “POST /api/v1/admin/approvals/approve”Purpose
Approve an approval request.
Body
{ "approvalId": "approval_123"}POST /api/v1/admin/approvals/reject
Section titled “POST /api/v1/admin/approvals/reject”Purpose
Reject an approval request.
Body
{ "approvalId": "approval_123", "rejectionReason": "Insufficient documentation"}10.2 Artists
Section titled “10.2 Artists”Artist management remains an Admin responsibility.
GET /api/v1/admin/artists
Section titled “GET /api/v1/admin/artists”Purpose
List artists.
Request body
- no body
Typical query parameters
active— boolean (1/0), independent ofstatusstatus—active|inactive|archived|sleeping. Omit to get everything exceptsleeping(the Artists backend hides sleeping artists by default at the entity level, the same way it hides non-city rows forcities). Admin’s Sleeping Artists tab (/artists/sleeping) passesstatus=sleepingexplicitly.iso2limitpagesearch— artist name and/or Spotify artist id (raw id or Spotify URL/URI)sort_bysort_order
POST /api/v1/admin/artists
Section titled “POST /api/v1/admin/artists”Purpose
Create a new artist.
Body
{ "name": "Artist Name", "type": "solo", "country": { "name": "Singapore", "iso2": "SG" }, "genre": ["pop"], "website": "https://artist.example", "bio": "Artist biography", "socials": { "instagram": "https://instagram.com/artist", "youtube": "https://youtube.com/@artist" }}GET /api/v1/admin/artists/{id}
Section titled “GET /api/v1/admin/artists/{id}”Purpose
Get one artist.
Request body
- no body
PATCH /api/v1/admin/artists/{id}
Section titled “PATCH /api/v1/admin/artists/{id}”Purpose
Update an artist. Partial — only fields present in the body are written.
Body
{ "name": "Updated Artist Name", "bio": "Updated bio", "website": "https://artist.example"}Sleep / wake an artist: PATCH with only {"status": "sleeping"} or {"status": "active"}.
The Admin frontend’s Sleep/Wake toggle (artist detail page) and bulk sleep/wake (Sleeping Artists
tab, and the main Artists list’s bulk-select bar) both call this — there is no dedicated
sleep/wake or bulk-status endpoint; bulk loops this PATCH per id, the same way DELETE /api/v1/admin/artists bulk-delete is a per-id loop under the hood.
DELETE /api/v1/admin/artists
Section titled “DELETE /api/v1/admin/artists”Purpose
Delete many artists (hard delete in Backend-Kisum-Artists Postgres). Blocked with a 409 if the artist has bookings, booking requests, or booking offers — remove/reassign those first. Available from the Admin artist detail page (single) and the Artists list bulk-select bar.
Body
{ "ids": ["artist_1", "artist_2"]}GET /api/v1/admin/artists/genres
Section titled “GET /api/v1/admin/artists/genres”Purpose
Search artist genres from Backend-Kisum-Artists PostgreSQL (genres table). Used by Admin genre pickers (Add Artist form, Artists list filter).
Query
| Param | Required | Notes |
|---|---|---|
q | yes (min 2 chars) | ILIKE search on genre name / slug; returns up to 50 matches ranked by exact name/slug match first, then prefix, then shorter names |
Request body
- no body
Response data
Array of genre name strings, e.g. ["Hip-Hop", "Hip Hop"]
GET /api/v1/admin/artists/next-id
Section titled “GET /api/v1/admin/artists/next-id”Purpose
Reserve the next numeric artist id for the Admin Add Artist form so S3 image upload can run before final save.
Request body
- no body
Response data
{ "id": 1616718 }The add form loads this on open and sends the same id in POST /api/v1/admin/artists so the created row matches the S3 path prefix (images/artists/<id>_…).
POST /api/v1/admin/artists/platforms
Section titled “POST /api/v1/admin/artists/platforms”Purpose
Get artist platform information from platform integrations.
Body
{ "platform": "music_brainz", "name": "Artist Name", "musicbrainz_id": "optional-mbid", "search_by_name": false, "spotify_id": "spotify-artist-id", "iso2": "SG", "type": "Person"}MusicBrainz behavior
- No
musicbrainz_id(orsearch_by_name: true) → search bynamewith optionallimit/offset(20 per page in the form picker). Returnsmusicbrainz_candidates[],musicbrainz_count, andmusicbrainz_offsetfor infinite-scroll picker UI. - With
musicbrainz_idandsearch_by_name: false→ fetch full MusicBrainz profile by MBID (country, city, genres/tags, sort name, active from, URL relations → platform links). - Admin UI: when a saved MusicBrainz ID exists, menu offers saved ID and search by name (for wrong-ID cases).
GET /api/v1/admin/artists/search
Section titled “GET /api/v1/admin/artists/search”Purpose
Search artists by name prefix or Spotify artist id (raw id, spotify:artist:…, or open.spotify.com/artist/…).
Query
search— artist name prefix and/or Spotify id
Request body
- no body
GET /api/v1/admin/artists/spotify
Section titled “GET /api/v1/admin/artists/spotify”Purpose
Look up artist information from Spotify via Backend-Kisum-MusicData (not direct Spotify credentials on Admin).
Query
name— artist name search (MusicDataGET /spotify/artists/search?q=…)spotify_id— single artist by Spotify id (MusicDataGET /spotify/artists/:id)limit— page size (default 10, max 20; MusicData cap)offset— Spotify search offset for pagination (default 0)exclude_existing— whentrue, omit Spotify rows that already have a Kisum artist link (edit-form picker). Default list search omits this so Open / Add can both appear.- If
nameis a raw Spotify id oropen.spotify.com/artist/…/spotify:artist:…, Admin treats it asspotify_id.
Behavior
- Returns all Spotify matches (unless
exclude_existing=true). Each item may include optionalkisumArtistIdwhen that Spotify platform id already exists in Kisum (legacy Mongo layers 1–5 + Backend-Kisum-Artists internalPOST /internal/artists/resolve-spotify-ids). Admin does not use Artists publicGET /artists?q=for this enrichment. - Admin UI: Add when
kisumArtistIdis absent; Open (artist detail) when present. Edit-form Spotify picker sendsexclude_existing=true. - Response includes standard list
meta(page,limit,total_items,total_pages) based on Spotify’s total hit count.
Upstream
- Spotify catalog: Backend-Kisum-MusicData (
MUSICDATA_INTERNAL_BASE_URL/MUSICDATA_BASE_URL,MUSICDATA_INTERNAL_API_KEY) - Kisum link resolution: Backend-Kisum-Artists (
ARTISTS_INTERNAL_BASE_URL,ARTISTS_INTERNAL_API_KEY) —POST /internal/artists/resolve-spotify-idswith body{ "spotify_ids": ["…"] }→{ "matches": { "<spotifyId>": "<kisumArtistId>" } }(numericartists.id, not uuid) - Admin does not connect to Artists Postgres directly.
Request body
- no body
10.2A Market directory
Section titled “10.2A Market directory”These routes proxy the current Backend-Kisum-Artists surface for non-artist artists-directory entities. Admin returns the Artists contract unchanged.
Runtime namespace
Section titled “Runtime namespace”/api/v1/admin/artists-directory/*
Current entity routes
Section titled “Current entity routes”GET /api/v1/admin/artists-directory/companiesPOST /api/v1/admin/artists-directory/companiesGET /api/v1/admin/artists-directory/companies/{id}PATCH /api/v1/admin/artists-directory/companies/{id}DELETE /api/v1/admin/artists-directory/companies/{id}GET /api/v1/admin/artists-directory/peoplePOST /api/v1/admin/artists-directory/peopleGET /api/v1/admin/artists-directory/people/{id}PATCH /api/v1/admin/artists-directory/people/{id}DELETE /api/v1/admin/artists-directory/people/{id}GET /api/v1/admin/artists-directory/genresPOST /api/v1/admin/artists-directory/genresGET /api/v1/admin/artists-directory/genres/{id}PATCH /api/v1/admin/artists-directory/genres/{id}DELETE /api/v1/admin/artists-directory/genres/{id}GET /api/v1/admin/artists-directory/subgenresPOST /api/v1/admin/artists-directory/subgenresGET /api/v1/admin/artists-directory/subgenres/{id}PATCH /api/v1/admin/artists-directory/subgenres/{id}DELETE /api/v1/admin/artists-directory/subgenres/{id}GET /api/v1/admin/artists-directory/platformsPOST /api/v1/admin/artists-directory/platformsGET /api/v1/admin/artists-directory/platforms/{id}PATCH /api/v1/admin/artists-directory/platforms/{id}DELETE /api/v1/admin/artists-directory/platforms/{id}GET /api/v1/admin/artists-directory/provider-sourcesPOST /api/v1/admin/artists-directory/provider-sourcesGET /api/v1/admin/artists-directory/provider-sources/{id}PATCH /api/v1/admin/artists-directory/provider-sources/{id}DELETE /api/v1/admin/artists-directory/provider-sources/{id}GET /api/v1/admin/artists-directory/regionsGET /api/v1/admin/artists-directory/regions/{id}GET /api/v1/admin/artists-directory/subregionsGET /api/v1/admin/artists-directory/subregions/{id}GET /api/v1/admin/artists-directory/countriesGET /api/v1/admin/artists-directory/countries/{id}GET /api/v1/admin/artists-directory/statesGET /api/v1/admin/artists-directory/states/{id}GET /api/v1/admin/artists-directory/citiesGET /api/v1/admin/artists-directory/cities/{id}
Current relation routes
Section titled “Current relation routes”GET /api/v1/admin/artists-directory/companies/{id}/peoplePUT /api/v1/admin/artists-directory/companies/{id}/peopleGET /api/v1/admin/artists-directory/companies/{id}/locationsPUT /api/v1/admin/artists-directory/companies/{id}/locationsGET /api/v1/admin/artists-directory/companies/{id}/genresPUT /api/v1/admin/artists-directory/companies/{id}/genresGET /api/v1/admin/artists-directory/companies/{id}/rosterPUT /api/v1/admin/artists-directory/companies/{id}/rosterGET /api/v1/admin/artists-directory/companies/{id}/social-metricsPUT /api/v1/admin/artists-directory/companies/{id}/social-metricsGET /api/v1/admin/artists-directory/people/{id}/companiesPUT /api/v1/admin/artists-directory/people/{id}/companiesGET /api/v1/admin/artists-directory/people/{id}/platform-accountsPUT /api/v1/admin/artists-directory/people/{id}/platform-accounts
Current geo nested routes
Section titled “Current geo nested routes”GET /api/v1/admin/artists-directory/regions/{id}/subregionsGET /api/v1/admin/artists-directory/regions/{id}/countriesGET /api/v1/admin/artists-directory/subregions/{id}/countriesGET /api/v1/admin/artists-directory/countries/{id}/statesGET /api/v1/admin/artists-directory/countries/{id}/citiesGET /api/v1/admin/artists-directory/states/{id}/cities
- These routes are for the market directory only, not for Core tenant-company administration.
- Upstream source of truth:
Backend-Kisum-Artists. - Admin → Artists auth (mandatory): machine-only via
ARTISTS_INTERNAL_API_KEY(X-Internal-API-Key). Admin does not mint Auth service tokens and must not forward the staff browserAuthorizationorx-orgon these proxies. Artists still accepts user JWT +x-orgfor persona apps; that path is unrelated to Admin. - Path IDs follow the upstream Market rules:
- UUIDs for market entities such as companies, people, genres, subgenres, platforms, and provider sources
- integers for geo/reference entities such as regions, subregions, countries, states, and cities
10.2B Promoter events (cross-tenant)
Section titled “10.2B Promoter events (cross-tenant)”Platform staff can list and manage all promoter events across every company. No x-org header — Admin proxies to Promoters internal machine routes.
Source of truth: Backend-Kisum-Promoters (events table).
Flow: browser → Backend-Kisum-Admin → Backend-Kisum-Promoters /internal/admin/events* (PROMOTERS_INTERNAL_API_KEY).
GET /api/v1/admin/events
Section titled “GET /api/v1/admin/events”Purpose
Paginated list of all promoter events (every status: pending, confirmed, completed, cancelled).
Query parameters
q— search (name, venue label, numeric id)status— filter by status (pending|confirmed|completed|cancelled)type— date bucket (all|today|upcoming|past; defaultall)sort_by—date(default) orcreated_atpage,limit
GET /api/v1/admin/events/{eventId}
Section titled “GET /api/v1/admin/events/{eventId}”Purpose
Single event detail (no finance fields).
Path
eventId— numeric Promoters event id (int32)
PATCH /api/v1/admin/events/{eventId}
Section titled “PATCH /api/v1/admin/events/{eventId}”Purpose
Staff edit of operational fields only: date, venue (Venues Postgres UUID string), lineup (array of Artists directory numeric ids).
DELETE /api/v1/admin/events/{eventId}
Section titled “DELETE /api/v1/admin/events/{eventId}”Purpose
Hard-delete one event (platform staff only).
10.3 Venues
Section titled “10.3 Venues”Venue management remains an Admin control-plane responsibility, but runtime venue truth now belongs to Backend-Kisum-Venues.
Current enforced integration path:
- browser ->
Frontend-Kisum-Admin - browser ->
Backend-Kisum-Admin Backend-Kisum-Admin->Backend-Kisum-Venuesinternal admin routes
Important rules:
- Admin must not query Venue Postgres directly
- numeric
venue_idis the Admin-facing venue identity - external venue enrichment/search may still stay in Admin, but persisted truth is written through Venue backend
GET /api/v1/admin/venues
Section titled “GET /api/v1/admin/venues”Purpose
List venues.
Runtime truth
- read from
Backend-Kisum-Venues - return both backend UUID
idand numericvenue_id
Request body
- no body
Typical query parameters
activeiso2limitpagesearchsort_bysort_ordertype
POST /api/v1/admin/venues
Section titled “POST /api/v1/admin/venues”Purpose
Create a venue.
Body
{ "name": "Kisum Arena", "country": "Singapore", "region": "Singapore", "city": "Singapore", "type": "arena", "capacity": 12000, "address": "120 Orchard Road"}GET /api/v1/admin/venues/{id}
Section titled “GET /api/v1/admin/venues/{id}”Purpose
Get one venue.
{id} is the numeric venue_id.
Request body
- no body
PATCH /api/v1/admin/venues/{id}
Section titled “PATCH /api/v1/admin/venues/{id}”Purpose
Update a venue.
{id} is the numeric venue_id.
Body
{ "name": "Updated Kisum Arena", "capacity": 15000, "type": "arena"}DELETE /api/v1/admin/venues
Section titled “DELETE /api/v1/admin/venues”Purpose
Delete many venues.
Body
{ "ids": ["1001", "1002"]}GET /api/v1/admin/venues/types
Section titled “GET /api/v1/admin/venues/types”Purpose
Get all venue types.
Runtime truth:
- read from Venue
venue_type
GET /api/v1/admin/venues/sleeping
Section titled “GET /api/v1/admin/venues/sleeping”Purpose
List sleeping imported venues that are still held by the Kisum holding company.
POST /api/v1/admin/venues/{id}/takeover-approvals
Section titled “POST /api/v1/admin/venues/{id}/takeover-approvals”Purpose
Create a Venue-owned takeover approval record for a sleeping venue.
POST /api/v1/admin/venues/{id}/activate
Section titled “POST /api/v1/admin/venues/{id}/activate”Purpose
Move a sleeping venue out of the Kisum holding company into the approved tenant company and mark it active.
Request body
- no body
POST /api/v1/admin/venues/{id}/revert-takeover
Section titled “POST /api/v1/admin/venues/{id}/revert-takeover”Purpose
Undo a previously approved venue takeover. Moves the venue back to the sleeping holding company, marks the originating approved takeover request revoked (with decision_notes = reason), and writes reconciliation + audit rows. Use case: falsified proof of ownership, or a takeover approved by mistake.
Request body
reason(required, min 10 chars): explanation surfaced to the tenant in the revoked request’sdecision_notes.actor_user_id,actor_email,actor_name— stamped by the handler from the JWT, not trusted from the client.
Errors
409 VENUE_HAS_TENANT_DATA(witherror.detailscarrying per-table counts forspaces,bookings,deposits,contracts,events,availability_blocks,checklists) — refuses when the tenant has created data on the venue. Admins must coordinate cleanup first.409 VENUE_NOT_ACTIVE— venue is not inownership_status='active'.409 VENUE_ALREADY_SLEEPING— venue already belongs to the sleeping holding company.404— venue not found.
10.3a Venue Requests (queue)
Section titled “10.3a Venue Requests (queue)”Admin BFF queue for tenant-submitted venue requests. Proxies the /internal/admin/venue-requests* family on Backend-Kisum-Venues. Decision routes stamp the JWT actor (user_id / user_email / user_name) onto the payload before forwarding.
GET /api/v1/admin/venue-requests
Section titled “GET /api/v1/admin/venue-requests”Purpose
List venue takeover + creation requests across all companies for the admin queue.
Query parameters
status(optional):pending|approved|rejected.kind(optional):takeover|creation.requestedCompanyId(optional): scope to a single tenant company.page,limit: pagination (defaults from the validator).
GET /api/v1/admin/venue-requests/{id}
Section titled “GET /api/v1/admin/venue-requests/{id}”Purpose
Read a single venue request including the kind-specific payload, the proofOfOwnership descriptor (if any), and any decision metadata.
GET /api/v1/admin/venue-requests/{id}/proof-url
Section titled “GET /api/v1/admin/venue-requests/{id}/proof-url”Purpose
Return a short-lived presigned S3 GET URL for the proof-of-ownership file attached to the request, so the admin UI can open it in a new tab without making the bucket public.
Response shape
url: presigned GET URL.expiresAt: ISO-8601 expiry (5 minutes from issue).fileName,contentType,size: descriptor copied from the request row.
Errors
404when no proof was uploaded with the request.
POST /api/v1/admin/venue-requests/{id}/approve
Section titled “POST /api/v1/admin/venue-requests/{id}/approve”Purpose
Approve a venue request. The Venue backend dispatches to the existing ActivateVenue (for takeover) or CreateVenue (for creation) flow, then marks the request approved.
Request body
decisionNotes(optional): free-form notes shown to the tenant.
POST /api/v1/admin/venue-requests/{id}/reject
Section titled “POST /api/v1/admin/venue-requests/{id}/reject”Purpose
Reject a venue request. Persists decisionNotes and marks the request rejected.
Request body
decisionNotes(optional, but recommended): explanation surfaced to the tenant in/settings/venue.
GET /api/v1/admin/venues/bandsintown
Section titled “GET /api/v1/admin/venues/bandsintown”Purpose
Search venues from Bandsintown.
Request body
- no body
GET /api/v1/admin/venues/viberate
Section titled “GET /api/v1/admin/venues/viberate”Purpose
Search venues from Viberate.
Request body
- no body
GET /api/v1/admin/venues/gemini
Section titled “GET /api/v1/admin/venues/gemini”Purpose
Generate or enrich venue information using Gemini.
Request body
- no body
GET /api/v1/admin/venues/search-by-platform
Section titled “GET /api/v1/admin/venues/search-by-platform”Purpose
Search venue-like records from an external source for admin auto-fill.
Request body
- no body
Query parameters (representative)
name(required): search string (venue name).platform(required): one ofviberate,bandsintown,gemini,musicbrainz,openstreetmap.page,limit: pagination.type,country,region,city: optional refinements. Forplatform=gemini,type,country, andregionare required (see handler validation). Forplatform=musicbrainz, onlynameis strictly required; the server mapsmusicbrainzto MusicBrainz Places (GET /ws/2/place) and may passcity/countryas optional Luceneareafilters.cityis forwarded for all platforms when present.
Notes
-
musicbrainz: responses includeplatform_idswithtype=musicbrainzand the place MBID, andurlpointing athttps://musicbrainz.org/place/<mbid>. The Admin BFF should send a descriptiveUser-Agent(config:MUSICBRAINZ_USER_AGENT) per MusicBrainz API policy. -
openstreetmap(2026-07-28): onlynameis strictly required (cityandcountrynarrow the search and should be passed when the form has them — a bare venue name searched worldwide is mostly noise). Responses includeplatform_idswithtype=openstreetmapand the OSM reference (way/123456), andurlpointing at the record on openstreetmap.org unless the venue publishes its own website.This is the only provider that reliably returns
country_iso2,region, andpostal_code— the block the venue form marks required and that most imported rows are missing — which is the reason it needs just a name rather than the full Country/Region/City set the other providers demand.Implementation notes that are contract, not detail:
- It talks to Nominatim, not Overpass. Overpass is a tag/area query engine; an unbounded
nwr["name"~"..."]search is a global regex scan the public endpoint throttles or refuses. Same OpenStreetMap data. OPENSTREETMAP_USER_AGENTis mandatory. The Nominatim usage policy bans anonymous clients and will block the source IP, so the client refuses to build without one and the endpoint answers400instead. One request per second is enforced in code. PointOPENSTREETMAP_BASE_URLat a self-hosted Nominatim to lift that cap.- Results are ODbL: the OpenStreetMap attribution is returned on each result and must remain attached wherever the data is displayed or stored.
- Capacity is never mapped onto the venue. OSM’s
capacitytag is usually a seated or sports-configuration figure, not the concert configuration a booking decision needs. - Only fields the provider actually resolved are returned, so applying a result never blanks data an admin already entered.
- It talks to Nominatim, not Overpass. Overpass is a tag/area query engine; an unbounded
Venue enrichment — “Fetch all data” (2026-07-28)
Section titled “Venue enrichment — “Fetch all data” (2026-07-28)”POST /api/v1/admin/venues/enrich — one venue, every public source, one call.
Returns per-field proposals with provenance, not a merged venue.
Sources: OpenStreetMap tags · Nominatim reverse geocode · BigDataCloud reverse geocode · Wikidata · Wikimedia Commons · Wikipedia.
Nothing is written. The endpoint proposes; the admin form applies only ticked fields and only into fields that are currently empty.
Auto-apply variant (2026-08-11): POST /api/v1/admin/venues/enrich-apply
uses the same enrichment pipeline but writes high-confidence proposals only
into empty venue fields server-side (no manual tick). Used by SoccerWiki
import Enrich imported after sleeping venues are created. Capacity and
existing main image are never overwritten; publishable Wikimedia images upload
to S3 when the venue has no image.
Which source wins which field, and why
Section titled “Which source wins which field, and why”Priority is per field because no single ranking is correct for everything. The reasons are measured, not assumed (Istanbul sample, 2026-07-28):
| Field | Winner | Reason |
|---|---|---|
| Address / postcode / country | Nominatim reverse | Derived from map geometry, so it answers for any coordinate. Only ~27% of venues carry a street tag and 6% a capacity. |
| City | BigDataCloud | Nominatim frequently returns the district — the reason a venue in Fatih reads as “Fatih” rather than “Istanbul”. |
| Website / phone | OpenStreetMap tags | The venue’s own record beats an encyclopedia entry. |
| Capacity / image / opening date | Wikidata | Nothing else carries them. |
Wikidata is never a source of addresses: measured 0% postal code, 8% street. It answers “what is this venue like”, not “where is it”.
Confidence, and why it is not cosmetic
Section titled “Confidence, and why it is not cosmetic”Each proposal is high or hint:
- A Wikidata item reached through the OSM
wikidatatag is an exact cross-reference →high. - The same item reached by searching the venue name →
hint, with a warning, because it may be a different venue entirely. - Capacity is always
hint, whatever the source: it is normally a seated or sports-configuration figure, not a concert configuration.
Only high values are pre-selected in the UI, so nothing unverified is applied
by accident. Disagreements between sources appear as alternatives rather than
being silently resolved.
Images
Section titled “Images”Returned separately from field proposals and never auto-applied. Commons files
carry per-file licences and most require crediting the photographer; the
licence, licence URL and attribution travel with every image. An image whose
licence could not be determined is marked publishable: false — treat that as
“do not publish”, never as permission.
Failure behaviour
Section titled “Failure behaviour”One source failing never fails the request. Every source reports ok /
skipped / failed / not_configured with a reason, so a blank field is
explainable: “Wikidata had nothing” and “Wikidata timed out” are different
answers.
Deliberately per-venue
Section titled “Deliberately per-venue”Bulk was ruled out. Nominatim’s one-request-per-second policy alone turns a catalogue-wide run into hours, and every value here wants human confirmation.
City names come from the Market directory
Section titled “City names come from the Market directory”The admin UI picks the city from the shared Market geo directory rather than accepting free text, so a swept city is spelled the same as everywhere else in Kisum and the Nominatim boundary lookup gets a consistent input. The city list is keyed by the numeric Market country id; the sweep itself needs the ISO2 code — the UI carries both from one country selection.
A city absent from the Market directory therefore cannot be swept by name; the country-wide sweep covers it instead.
No AI in this path (yet)
Section titled “No AI in this path (yet)”The merge is deterministic. Every source already returns structured fields, so a
model between structured input and structured output would cost provenance and
risk corrupting exact values such as coordinates. AI is planned only for the
Wikipedia-derived description (prose, no structured source) and for
disambiguating which candidate is the right venue. The endpoint returns
raw per-source payloads so that pass can reuse the same fetch.
Config: WIKIMEDIA_USER_AGENT (falls back to OPENSTREETMAP_USER_AGENT),
WIKIDATA_BASE_URL, BIGDATACLOUD_BASE_URL.
Area venue discovery (2026-07-28)
Section titled “Area venue discovery (2026-07-28)”Sweeps OpenStreetMap for every venue inside a city or a country, so the
catalogue can be populated from an area rather than one venue name at a time.
This is the discovery half of external venue data; search-by-platform is the
lookup half.
| Endpoint | Purpose |
|---|---|
GET /api/v1/admin/venues/discovery/categories | Venue kinds a sweep can cover |
POST /api/v1/admin/venues/discovery | Start a sweep — countryCode required, city optional; returns 202 + job id |
GET /api/v1/admin/venues/discovery/{jobId} | Progress while running, candidates when complete |
POST /api/v1/admin/venues/discovery/{jobId}/import | Create the selected candidates |
Overpass here, Nominatim in search-by-platform. They answer different
questions: Nominatim resolves a typed name, Overpass returns everything inside a
boundary. Area discovery is the case where Overpass is the correct tool. For a
city sweep both are used — Nominatim resolves the city’s boundary relation
first, because area["name"="Istanbul"] inside Overpass is ambiguous across
countries and administrative levels. A city sweep therefore needs
OPENSTREETMAP_USER_AGENT and OVERPASS_USER_AGENT; a country sweep needs
only the latter.
Contract points that are not implementation detail:
- It is a job, not a request. A country sweep runs one Overpass query per venue category because a single all-categories country query exceeds the endpoint’s timeout. Callers poll. City sweeps use the identical path and finish sooner.
- A failed category does not fail the sweep. Partial results are returned and the failure is recorded against that category.
- Import carries external ids only. Venue data comes from the stored sweep, so the endpoint cannot be used to inject a hand-crafted venue.
- Import is idempotent on
(external_source, external_source_id): re-running returns the same venues withcreated: falserather than duplicating. - Imported venues are created sleeping + claimable under the holding company (identical to the China catalogue import), so the real operator can still claim them through the takeover flow.
- Candidates already in Kisum are flagged, not filtered out — a re-sweep of a covered city must read as “nothing new”, not as an empty result.
- “Already in Kisum” means the whole venue table, not just rows Kisum imported
from OpenStreetMap. Two passes: an exact OpenStreetMap id match (certain), then
a name-similarity-plus-proximity match via
POST /internal/venues/matchon the Venues service. Checking lineage alone answered “did this come from OpenStreetMap?” and reported every pre-existing venue as new. - A likely match stays importable, with the matched venue and the evidence shown, because fuzzy matching cannot be certain and hiding a genuinely new venue is the worse failure. Only an exact id match is locked.
- Capacity travels as an unconfirmed hint and is never written onto a venue.
- Results are ODbL; the attribution is returned on the job and must be shown.
- Sweeps live in memory for an hour. The durable outcome is the venues created.
Venue import from external platforms (2026-08-11)
Section titled “Venue import from external platforms (2026-08-11)”Staff choose a data source platform (SoccerWiki only today; more later),
upload a platform export in the Admin UI at /venues/venue-import (parsed
in the browser only), select rows, click Get data to stage in Venues
Postgres and scrape when needed, review duplicates, then import only
non-duplicated rows as sleeping + claimable venues.
Admin UI list filter (2026-08-11): after upload / Get data, the table defaults to Ready (not duplicate, not imported). Separate filters: Duplicates, Imported (already in venues — use for Enrich), and All. Import runs one row at a time with live progress and per-row errors. Staff can press Stop to cancel the remaining queue (already-created venues are kept).
Staging keys: venue_temp_rows is keyed by platform + platform_id
(unique together). SoccerWiki uses platform=soccerwiki and
platform_id=<StadiumData.ID>. One saved row per external record — re-uploads
reuse cached scrape data unless staff click Force update on selected rows.
| Endpoint | Purpose |
|---|---|
POST /api/v1/admin/venues/venue-imports/lookup-saved | { platform, platformIds[] } — return existing staging rows (max 500 ids) so the UI can show Saved without re-scraping |
POST /api/v1/admin/venues/venue-imports/stage-and-scrape | Primary flow. { platform, batchId?, filename, forceScrape?, stadiums: [{ id, name }] } (max 100) — upsert staging rows, scrape only when not saved (or when forceScrape=true), resolve geo, auto-label duplicates; returns { batch, rows } |
POST /api/v1/admin/venues/venue-imports | Legacy: create a full batch from JSON (avoid from UI; use stage-and-scrape instead) |
GET /api/v1/admin/venues/venue-imports/{batchId} | Batch metadata |
GET /api/v1/admin/venues/venue-imports/{batchId}/rows?page&limit=100&q= | Paginated staging rows |
POST /api/v1/admin/venues/venue-imports/{batchId}/rows/scrape | Re-scrape existing staged { rowIds } (max 100) without adding new stadium ids |
PATCH /api/v1/admin/venues/venue-imports/{batchId}/rows/{rowId} | Manual { isDuplicated } — exact external-id matches stay locked |
POST /api/v1/admin/venues/venue-imports/{batchId}/import | Import selected row ids via Venues external intake (source=<platform>, e.g. soccerwiki, externalId=<platform id>). Fast DB path only: writes sleeping venue + capacity from staged row; does not download images (use Enrich imported / enrich-apply for hero image and other public fields). |
POST /api/v1/admin/venues/venue-imports/{batchId}/rows/reconcile | Re-run duplicate matching for already-scraped rows (e.g. after matcher fixes) |
POST /api/v1/admin/venues/enrich-apply | Post-import enrichment. { venueId, name, city, country, … } — same inputs as POST /venues/enrich; fetches public sources and writes only high confidence values into empty fields. Never overwrites existing capacity or main image. Publishable Wikimedia image uploads to S3 when the venue has no image. Admin UI Enrich imported runs this sequentially for selected imported rows. |
Ownership split:
- JSON upload stays in the browser until Get data; Postgres staging
is written only for selected stadiums (temp tables
venue_temp_batches,venue_temp_rows) — not Mongo. - HTML scrape for SoccerWiki runs in Admin (
SOCCERWIKI_BASE_URL,SOCCERWIKI_USER_AGENT, ~1 req/s); Admin writes parsed fields to Venues staging via internal API. Other platforms will plug in the same BFF shape. - Geo resolves country (FIFA → ISO3 → Artists) and city name under country.
- Duplicate rules mirror Discover: exact
(platform, id)in catalog, thenPOST /internal/venues/matchname similarity. Only non-duplicated rows import.
Venues internal mirror (machine key): /internal/admin/venue-imports/*.
Breaking rename (2026-08-11): paths and packages were renamed from
soccerwiki-imports / soccerwikiimport to venue-imports / venueimport.
The SoccerWiki platform id and scrape env vars stay soccerwiki /
SOCCERWIKI_*.
10.4 Files
Section titled “10.4 Files”POST /api/v1/admin/files/upload
Section titled “POST /api/v1/admin/files/upload”Purpose
Upload file assets for admin workflows.
Target
- operational upload only
- not a replacement for Core company-document truth
Body
multipart/form-data
Required form field:
file
Venue uploads (existing):
venue_id— venue numeric id (Mongo)- optional
kind—media(default,images/venues/media) orlogo(images/venues/logos)
Artist image uploads:
artist_id— artist numeric idkind—artist(stores underimages/artists/<artist_id>_<n><ext>)
Max file size: 5 MB.
10.5 AI
Section titled “10.5 AI”POST /api/v1/admin/ai/gemini
Section titled “POST /api/v1/admin/ai/gemini”Purpose
Staff-only AI helper route.
Body
{ "prompt": "Summarize this admin review case"}11. Implementation priority
Section titled “11. Implementation priority”If implementing the new Admin system, the recommended order is:
- package/module/add-on management
- company create/update and company lifecycle orchestration
- subscription/add-on assignment and entitlement inspection
- delegation-rule management through Auth
- payment-provider configuration
- cleanup or deprecation of legacy company proxy flows
12. Final rule
Section titled “12. Final rule”Admin endpoints must always be documented with:
- what the admin action does
- which system stores the truth
- what request body is required
- what downstream effect should happen
If an Admin endpoint writes commercial truth, it should target Core.
If an Admin endpoint writes access truth, it should target Auth.