Company tenancy, branches, employees, and roles¶
This document is the v2 REST contract for the company tenant model. All company routes require a bearer token with JWT role COMPANY and an active CompanyMembership. The server derives Company.id from that membership; do not send a company context ID on company-side routes.
Resource model¶
Company
├── Branches
│ ├── operational address
│ ├── bank accounts
│ └── Programs
│ ├── subscriptions
│ ├── cycles
│ └── auctions
└── Employees
└── scoped role assignments
Every company has exactly one live headquarters branch. Every program belongs to exactly one company and one branch in that company. Auction ownership is resolved through Auction -> ProgramCycle -> Program; the auction does not duplicate company or branch ownership.
Branch endpoints¶
Base path: /v2/companies/branches
| Method | Path | Permission | Behavior |
|---|---|---|---|
POST | / | branch.create | create an active branch; returns 201 |
GET | / | branch.read in effective scope | paginated/searchable scoped list |
GET | /:branchId | branch.read | branch detail and readiness |
PATCH | /:branchId | branch.profile.manage | update name, code, or contact fields |
PUT | /:branchId/address | branch.address.manage | create or replace operational address |
POST | /:branchId/deactivate | branch.lifecycle.manage | set INACTIVE |
POST | /:branchId/reactivate | branch.lifecycle.manage | set ACTIVE |
DELETE | /:branchId | branch.lifecycle.manage | soft-archive an eligible branch |
List query parameters are pageNumber, pageSize, optional search, and optional status (ACTIVE, INACTIVE, or ARCHIVED). Search matches branch name and normalized code. Results are restricted to the actor's effective company/branch grants.
Create example:
{
"name": "Kochi Central",
"branchCode": "kl-01",
"email": "kochi@example.com",
"mobileNumber": "9876543210",
"countryCode": "IN",
"address": {
"line1": "MG Road",
"pincode": "682016",
"city": "Kochi",
"district": "Ernakulam",
"state": "Kerala"
}
}
The stored code is KL-01.
Branch code rules¶
branchCode is optional on create and update:
- surrounding whitespace is removed and letters are uppercased;
- an empty string is stored as
null; - length is 1-20 characters when present;
- only uppercase letters, digits, and internal hyphens are allowed;
- leading, trailing, or repeated hyphens are rejected;
- a non-deleted code is unique within its company;
- the same code may exist in another company;
- duplicate normalized codes return
409 Conflict.
Clear a code with:
Readiness and lifecycle¶
isProgramReady is computed, not stored. It is true only when the branch is active, not archived/deleted, and has an operational address. A verified bank account is not required.
The headquarters branch may be renamed and assigned a code, but cannot be archived. A non-headquarters branch cannot be archived while it has a non-terminal program. Archive sets status=ARCHIVED and deletedAt; it does not hard-delete the row.
Branch bank accounts¶
Base path: /v2/companies/branches/:branchId/bank-accounts
| Method | Path | Behavior |
|---|---|---|
GET | / | paginated branch account list |
POST | / | create account; returns 201 |
PATCH | /:bankAccountId | update account |
DELETE | /:bankAccountId | soft-delete account |
All operations require branch.banking.manage on the selected branch. Account numbers are unique per branch, and a partial unique index permits only one non-deleted primary account per branch.
Program ownership¶
branchId is required in step 1 of every new or existing program creation flow, including superadmin program creation. The branch must:
- belong to the program company;
- be active and not archived/deleted;
- be program-ready; and
- be within the employee's effective scope for company-side creation.
Company and admin program responses include branchId, branch name, and optional branch code. Company-side program lists and dependent auction, subscription, request, payment, document, and cycle operations are filtered by effective branch/program access.
Transfer a draft program¶
POST /v2/companies/programs/:programId/transfer
Authorization: Bearer <token>
Content-Type: application/json
{ "branchId": "<destination-branch-id>" }
The actor needs program.transfer on both source and destination branches. The destination must be program-ready. Transfer is allowed only when the program is INCOMPLETE or DRAFTED and has no enrolled subscribers, cycles, or auctions. The update and audit record are committed atomically.
Employee invitations¶
Base path: /v2/companies/employees/invitations
| Method | Path | Permission | Behavior |
|---|---|---|---|
POST | / | employee.invite | create token + OTP invitation; returns 201 |
GET | / | employee.read | paginated list, optionally filtered by status |
POST | /:invitationId/resend | employee.invite | rotate token/OTP and extend expiry |
POST | /:invitationId/revoke | employee.invite | revoke a pending invitation |
Invitation example:
{
"name": "Auction Operator",
"email": "operator@example.com",
"mobileNumber": "9876543210",
"countryCode": "IN",
"assignments": [
{
"roleId": "role_auction_operator_branch",
"scopeType": "BRANCH",
"branchId": "<branch-id>"
}
]
}
Every proposed role and resource scope must be assignable by the inviter. OWNER is never accepted here. A token is random, stored only as an HMAC hash, and expires after 72 hours. OTP delivery uses the existing SMS provider flow; email delivery uses the existing queue. Resend is limited to once per minute and rotates both token and expiry.
Acceptance is an auth route and does not require an existing session:
POST /v2/auth/employee-invitations/accept
Content-Type: application/json
{
"token": "<64-character-token>",
"otp": "123456",
"name": "Employee Name"
}
Token validity, expiry, OTP, identity uniqueness, company state, membership, and assignments are checked before the invitation is consumed. User creation or attachment, membership creation, role assignments, invitation consumption, and audit logging occur in one transaction. A replay returns a conflict and cannot create a second membership.
Admin invitation list¶
Superadmins read any company's invitations without a company session:
GET /v2/admin/companies/:companyId/invitations?pageNumber=1&pageSize=10&status=PENDING&sortBy=createdAt&sortOrder=desc
Authorization: Bearer <superadmin-token>
Query parameters are pageNumber (default 1), pageSize (1–100, default 10), optional status (PENDING, ACCEPTED, REVOKED, EXPIRED), and optional sortBy (createdAt, status, expiresAt) with sortOrder (asc, desc). The response returns { count, pageNumber, pageSize, invitations } with the same invitation objects as the company route (token hashes are never serialized). A missing company returns 404.
Employee and assignment endpoints¶
Base path: /v2/companies/employees
| Method | Path | Permission | Behavior |
|---|---|---|---|
GET | / | employee.read | memberships, assigned roles/scopes, and effective access |
POST | /:membershipId/suspend | employee.suspend | suspend and revoke sessions |
POST | /:membershipId/reactivate | employee.suspend | reactivate membership |
POST | /:membershipId/revoke | employee.suspend | revoke membership and sessions |
POST | /:membershipId/assignments | employee.assignment.manage | add scoped role assignment |
DELETE | /:membershipId/assignments/:assignmentId | employee.assignment.manage | remove assignment |
POST | /:membershipId/transfer-ownership | ownership.transfer | atomically transfer OWNER |
Assignment body shapes:
Exactly the target for the selected scope is allowed. Membership list responses separate stored assignments from computed effective permissions. Suspending or revoking an employee invalidates cached authorization and active sessions immediately.
Current membership¶
Returns the caller's active CompanyMembership with stored assignments and computed effective permissions. It requires the COMPANY persona and an active membership.
Admin company name lists¶
Superadmin routes under /v2/admin/companies:
| Method | Path | Behavior |
|---|---|---|
GET | / | paginated full company list |
GET | /minimal | paginated minimal company list |
GET | /names | paginated id + name list with search |
POST | / | provision a company; returns 201 |
GET /names accepts pageNumber, pageSize, and optional search, and returns { count, pageNumber, pageSize, totalPages, companies: [{ id, name }] }.
Role endpoints¶
Base path: /v2/companies/roles
| Method | Path | Permission | Behavior |
|---|---|---|---|
GET | /permissions | role.manage | immutable permission catalog |
GET | / | role.manage | built-in roles plus company custom roles |
POST | / | role.manage | create reusable custom role |
PATCH | /:roleId | role.manage | update custom role metadata/permissions |
DELETE | /:roleId | role.manage | delete an unassigned custom role |
Custom role example:
{
"name": "Selected Program Collections",
"description": "Handles payments and statements for assigned programs",
"scopeType": "PROGRAM",
"permissionIds": ["perm_program_read", "perm_statement_manage", "perm_payment_manage"]
}
Built-in system roles¶
The platform provisions 12 immutable system roles across three scopes:
| Role ID | Name | Scope | System role enum | Core purpose |
|---|---|---|---|---|
role_owner_company | Owner | COMPANY | OWNER | Full company access including ownership transfer |
role_company_admin_company | Company Admin | COMPANY | COMPANY_ADMIN | Full company access except ownership transfer |
role_branch_manager_branch | Branch Manager | BRANCH | BRANCH_MANAGER | Manage branch profile, address, banking, and programs at branch |
role_program_manager_branch | Program Manager (Branch) | BRANCH | PROGRAM_MANAGER | Manage all programs and program lifecycle under an assigned branch |
role_program_manager_program | Program Manager (Program) | PROGRAM | PROGRAM_MANAGER | Manage program lifecycle, status, documents for a specific program |
role_auction_operator_branch | Auction Operator (Branch) | BRANCH | AUCTION_OPERATOR | Schedule, operate, and bid manage auctions across a branch |
role_auction_operator_program | Auction Operator (Program) | PROGRAM | AUCTION_OPERATOR | Schedule, operate, and bid manage auctions for a specific program |
role_subscription_manager_branch | Subscription Manager (Branch) | BRANCH | SUBSCRIPTION_MANAGER | Manage subscribers, payments, and statements across a branch |
role_subscription_manager_program | Subscription Manager (Program) | PROGRAM | SUBSCRIPTION_MANAGER | Manage subscribers, payments, and statements for a specific program |
role_viewer_company | Viewer (Company) | COMPANY | VIEWER | Read-only access across all company resources |
role_viewer_branch | Viewer (Branch) | BRANCH | VIEWER | Read-only access across assigned branch and its programs |
role_viewer_program | Viewer (Program) | PROGRAM | VIEWER | Read-only access for a specific program and its auctions |
Permission catalog¶
The flat catalog returned by GET /v2/companies/roles/permissions contains 40 granular permissions:
- Company scope:
company.profile.read,company.profile.update,company.compliance.manage,company.mou.manage,company.policy.manage,company.billing.manage,branch.create,employee.read,employee.invite,employee.suspend,employee.assignment.manage,role.manage,ownership.transfer,template.manage,preset.manage,security-type.manage. - Branch scope:
branch.read,branch.profile.manage,branch.address.manage,branch.banking.manage,branch.lifecycle.manage,program.create,program.transfer. - Program scope:
program.read,program.update,program.status.manage,program.archive,subscription.manage,subscriber.manage,statement.manage,payment.manage,auction.read,auction.configure,auction.schedule,auction.operate,auction.bid.manage,auction.prebid.manage,auction.winner.manage,document.manage,signature.manage.
Granular endpoint permission reference¶
| Category | Endpoint(s) | Required permission | Scope |
|---|---|---|---|
| Profile & MOU | GET /v2/companies/me, /me/new, /me/mou | company.profile.read | COMPANY |
PATCH /v2/companies/me | company.profile.update | COMPANY | |
POST /v2/companies/me/mou/accept | company.mou.manage | COMPANY | |
PATCH /v2/companies/me/managers/:id | company.compliance.manage | COMPANY | |
POST /v2/companies/me/file-uploads/* | company.compliance.manage | COMPANY | |
| Dashboard | GET /v2/companies/dashboard | program.read AND subscriber.manage | COMPANY |
GET /v2/companies/dashboard/overview | program.read, subscriber.manage, company.billing.manage, employee.read | COMPANY | |
GET /v2/companies/dashboard/revenue, /billing | company.billing.manage | COMPANY | |
GET /v2/companies/dashboard/profile-health | company.profile.read | COMPANY | |
| Billing & Config | GET /v2/companies/billing/invoices, /billing/transactions | company.billing.manage | COMPANY |
GET/PUT /v2/companies/company-auction-policy (and /resolved) | company.policy.manage | COMPANY | |
GET/POST/PATCH/DELETE /v2/companies/auction-presets | preset.manage | COMPANY | |
GET/POST/DELETE /v2/companies/prebid-templates | template.manage | COMPANY | |
GET/POST/PATCH/DELETE /v2/companies/security-types | security-type.manage | COMPANY | |
| Documents | POST /v2/companies/programs/file-uploads/* | document.manage | PROGRAM |
GET/POST /v2/companies/documents/signatures/* | signature.manage | COMPANY | |
POST /v2/companies/me/programs/:id/consent | signature.manage | PROGRAM | |
| Requests | GET/PATCH /v2/companies/requests/:id | subscriber.manage | PROGRAM |
GET /v2/companies/programs/:id/requestors | subscriber.manage | PROGRAM | |
| Programs | POST .../programs/new/steps/1, .../existing/steps/1 | program.create | BRANCH |
POST .../programs/new/:id/steps/2 | program.update | PROGRAM | |
POST .../programs/existing/:id/steps/3 | subscriber.manage | PROGRAM | |
POST .../programs/existing/:id/steps/4, steps/5 | program.update | PROGRAM | |
PATCH /v2/companies/me/programs/:id | program.update | PROGRAM | |
PATCH /v2/companies/programs/:id/program-status | program.status.manage | PROGRAM | |
GET /v2/companies/programs/:id/settings | auction.read | PROGRAM | |
PATCH /v2/companies/programs/:id/settings | auction.configure | PROGRAM | |
POST /v2/companies/programs/:id/transfer | program.transfer | BRANCH (both) | |
| Cycles & Billing | POST /v2/companies/cycles, PATCH /v2/companies/cycles/:id | program.status.manage | PROGRAM |
POST /v2/companies/cycles/:id/invoices | payment.manage | PROGRAM | |
GET /v2/companies/cycles/:id/invoices (and /program-details) | statement.manage | PROGRAM | |
POST /v2/companies/cycles/:id/winners | auction.winner.manage | PROGRAM | |
POST /v2/companies/invoices/:id/mark-as-paid, /cancel | payment.manage | PROGRAM | |
GET /v2/companies/programs/:id/subscribers/:subId/statements | statement.manage | PROGRAM |
Errors and tenant privacy¶
| Status | Typical cause |
|---|---|
400 | invalid code/scope, branch not ready, or ineligible transfer |
401 | missing JWT or no active company membership |
403 | missing company-level administrative permission |
404 | missing or inaccessible tenant resource (prevents enumeration across scopes) |
409 | duplicate code/invitation/role, last-owner guard, or assigned-role deletion |
429 | invitation resend attempted within 60 seconds |
[!IMPORTANT] 403 vs 404 security design:
403 Forbiddenis returned for company-wide administrative gates (e.g. attempting to call/v2/companies/roleswithoutrole.manage, or calling a company route when lacking a company-scoped grant).404 Not Foundis deliberately returned when trying to access or manipulate a scoped resource (branch, program, cycle, invoice, auction, or request) where the actor lacks the required permission at that specific resource's branch or program scope. This prevents cross-tenant and cross-branch enumeration attacks.
Client requirements¶
- Treat
Company.id, not the company login user'sUser.id, as the tenant ID. - Load branches before creating a program and send
branchIdin step 1. - Display branch name/code returned by program and auction list/detail APIs.
- Use employee invitations and scoped assignments for all staff access.
- Handle
404for out-of-scope tenant resources without revealing guessed IDs.