Skip to content

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:

{ "branchCode": null }

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:

{ "roleId": "<company-role>", "scopeType": "COMPANY" }
{ "roleId": "<branch-role>", "scopeType": "BRANCH", "branchId": "<branch-id>" }
{ "roleId": "<program-role>", "scopeType": "PROGRAM", "programId": "<program-id>" }

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

GET /v2/companies/me/membership
Authorization: Bearer <token>

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 Forbidden is returned for company-wide administrative gates (e.g. attempting to call /v2/companies/roles without role.manage, or calling a company route when lacking a company-scoped grant).
  • 404 Not Found is 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

  1. Treat Company.id, not the company login user's User.id, as the tenant ID.
  2. Load branches before creating a program and send branchId in step 1.
  3. Display branch name/code returned by program and auction list/detail APIs.
  4. Use employee invitations and scoped assignments for all staff access.
  5. Handle 404 for out-of-scope tenant resources without revealing guessed IDs.