Company addresses architecture — frontend integration¶
This guide covers the company registered and billing address architecture in v2, how company-level addresses are cleanly decoupled from branch operational addresses, and how frontends must integrate the write and read paths across superadmin provisioning, self-service onboarding, company profile settings, and the public programs catalog.
1. Overview and address model¶
In Dichit, addresses are strictly partitioned into three independent concepts:
flowchart TD
subgraph Company Entity
C[Company: id, name, gstin, pan]
CRA["Registered Address (company_addresses: REGISTERED)"]
CBA["Billing Address (company_addresses: BILLING)"]
C --> CRA
C --> CBA
end
subgraph Branch Entities
HQ["Headquarters Branch (branches: isHeadquarters=true)"]
BR["Regional Branch (branches: isHeadquarters=false)"]
HQ_ADDR["Branch Operational Address (branch_addresses)"]
BR_ADDR["Branch Operational Address (branch_addresses)"]
HQ --> HQ_ADDR
BR --> BR_ADDR
end
C -. owns .-> HQ
C -. owns .-> BR The three address dimensions¶
| Address Type | Table | Ownership Scope | Managed via | Purpose |
|---|---|---|---|---|
| Branch Operational Address | branch_addresses | branchId (1 per branch) | PUT /v2/companies/branches/:branchId/address | Physical branch location, customer visits, program hosting readiness (isProgramReady). |
| Company Registered Address | company_addresses (type: REGISTERED) | companyId (1 per company) | Provisioning / Onboarding Step 3 / PATCH /v2/companies/me/addresses | Official legal corporate address recorded with ROC / Sub-Registrar Office. |
| Company Billing Address | company_addresses (type: BILLING) | companyId (1 per company) | Provisioning / Onboarding Step 3 / PATCH /v2/companies/me/addresses | Corporate tax, GST billing, and invoicing address. |
Key architectural invariants for frontends¶
- Complete Independence: Company registered and billing addresses belong to the company aggregate, not to any individual branch or employee user account. Updating a branch's operational address never alters the company's registered or billing address, and vice versa.
- Headquarters Branch: During company creation or onboarding, the initial operational address is applied to the default Headquarters branch, while registered and billing addresses are saved into
company_addresses. - Subscriber addresses: the
addressestable holds only subscriber addresses (SUBSCRIBER_PERMANENT,SUBSCRIBER_PRESENT). Company users never have records in theaddressestable.
2. Address routes¶
PATCH /v2/companies/me/addressesupdates corporate addresses incompany_addresses(REGISTEREDorBILLING) by passingaddressId. Branch operational addresses are updated per branch viaPUT /v2/companies/branches/:branchId/address.POST /v1/addressaccepts onlySUBSCRIBER_PERMANENTandSUBSCRIBER_PRESENT.- Company addresses use the dedicated enum
CompanyAddressType(REGISTERED|BILLING).
3. Write paths¶
3.1 Superadmin company provisioning¶
Request payload¶
{
"name": "Acme Chits Private Limited",
"mobileNumber": "+919876543210",
"email": "contact@acmechits.com",
"countryCode": "IN",
"cin": "U65992KL2024PTC123456",
"pan": "ABCDE1234F",
"registrationNumber": "REG-KL-2024-001",
"sroId": 12,
"branch": "Kochi Head Office",
"hasGstin": true,
"gstin": "32ABCDE1234F1Z5",
"billingAddress": {
"line1": "Acme Towers, MG Road",
"line2": "Ravipuram",
"pincode": "682016",
"city": "Kochi",
"district": "Ernakulam",
"state": "Kerala",
"cityId": 101,
"postOfficeId": 205
},
"registeredSameAsBillingAddress": false,
"registeredAddress": {
"line1": "Suite 401, Corporate Plaza",
"line2": "Marine Drive",
"pincode": "682031",
"city": "Kochi",
"district": "Ernakulam",
"state": "Kerala",
"cityId": 101,
"postOfficeId": 210
},
"yearOfEstablishment": 2018,
"noOfOngoingPrograms": 0,
"noOfCompletedPrograms": 0,
"manager": {
"name": "Jane Doe",
"mobileNumber": "+919876543211",
"email": "jane.doe@acmechits.com",
"countryCode": "IN",
"pan": "FGHIJ5678K",
"aadhaarNumber": "123456789012"
},
"requiresSubAccount": false
}
Address validation rules for provisioning forms¶
billingAddressis mandatory.registeredSameAsBillingAddressdefaults tofalse.- When
registeredSameAsBillingAddress === true: - The form must omit
registeredAddress(or sendundefined). Sending an object will trigger a validation error:"Registered address must be omitted when it matches the billing address." - The backend automatically copies
billingAddressintocompany_addresseswithtype: 'REGISTERED'. - When
registeredSameAsBillingAddress === false: registeredAddressis mandatory. Omitting it will trigger:"Registered address is required when it differs from the billing address."- In both cases, the backend initializes the headquarters branch operational address using
billingAddress.
3.2 Self-service onboarding (Step 3: BUSINESS_DETAILS)¶
Active onboarding companies complete Step 3 (BUSINESS_DETAILS) via the dedicated step endpoint:
POST /v2/auth/companies/onboarding/steps/3
Authorization: Bearer <company-access-token>
Content-Type: application/json
Request payload (when registered address matches billing address)¶
{
"hasGstin": true,
"gstin": "32ABCDE1234F1Z5",
"billingAddress": {
"line1": "Acme Towers, MG Road",
"line2": "Ravipuram",
"pincode": "682016",
"city": "Kochi",
"district": "Ernakulam",
"state": "Kerala",
"cityId": 101,
"postOfficeId": 205
},
"registeredSameAsBillingAddress": true
}
Request payload (when registered address differs from billing address)¶
{
"hasGstin": true,
"gstin": "32ABCDE1234F1Z5",
"billingAddress": {
"line1": "Acme Towers, MG Road",
"line2": "Ravipuram",
"pincode": "682016",
"city": "Kochi",
"district": "Ernakulam",
"state": "Kerala",
"cityId": 101,
"postOfficeId": 205
},
"registeredSameAsBillingAddress": false,
"registeredAddress": {
"line1": "Suite 401, Corporate Plaza",
"line2": "Marine Drive",
"pincode": "682031",
"city": "Kochi",
"district": "Ernakulam",
"state": "Kerala",
"cityId": 101,
"postOfficeId": 210
}
}
Field specifications¶
| Field | Type | Required | Description |
|---|---|---|---|
hasGstin | boolean | Optional (default: false) | Indicates whether the company has a registered GSTIN. |
gstin | string | Conditional | Required if hasGstin === true. Must match valid Indian GSTIN format. |
billingAddress | object | Yes | Full address object (line1, line2, pincode, postOffice, city, district, state, cityId, postOfficeId). |
registeredSameAsBillingAddress | boolean | Optional (default: false) | Indicates whether legal registered address is identical to billing address. |
registeredAddress | object | Conditional | Required when registeredSameAsBillingAddress === false. Must be omitted when registeredSameAsBillingAddress === true. |
Validation rules (OnboardingStep3Validator)¶
- If
hasGstin === trueandgstinis missing: returns"GSTIN is required if you have one." - If
registeredSameAsBillingAddress === false(or omitted) andregisteredAddressis missing: returns"Registered address is required if it is not the same as billing address". - If
registeredSameAsBillingAddress === trueandregisteredAddressis provided: returns"Registered address should not be provided when it is same as billing address". - Both
billingAddressandregisteredAddressrequire positive integers forcityIdandpostOfficeId.
Response (200 OK)¶
{
"status": "success",
"code": 200,
"message": "Nice progress — let's move to the next step.",
"data": {
"completedStep": "BUSINESS_DETAILS",
"nextStep": "BANK_DETAILS"
},
"error": null
}
Side effects performed by backend¶
- Updates the headquarters branch's operational physical address in
branch_addressesusingregisteredAddress(orbillingAddressif matching). - Upserts
company_addresseswithtype: 'REGISTERED'. - Upserts
company_addresseswithtype: 'BILLING'. - Updates company record with
onboardingStep = 'BUSINESS_DETAILS'and advancesnextStepto'BANK_DETAILS'.
3.3 Updating corporate addresses (registered or billing)¶
Active companies can update their registered or billing address via PATCH /v2/companies/me/addresses:
PATCH /v2/companies/me/addresses
Authorization: Bearer <company-token>
Content-Type: application/json
Request payload¶
{
"addressId": "ca_cl0123456789abcdefghijk",
"line1": "Suite 402, Corporate Plaza",
"line2": "Marine Drive",
"pincode": "682031",
"city": "Kochi",
"district": "Ernakulam",
"state": "Kerala",
"postOffice": "Marine Drive SO",
"cityId": 101,
"postOfficeId": 210
}
Field specifications¶
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
addressId | string | Yes | Valid CUID | ID of the company_addresses record to update (obtained from GET /v2/companies/me under registeredAddress.id or billingAddress.id). |
line1 | string | Optional | Min 1 char | Address line 1 (building, street). |
line2 | string | Optional | Min 1 char | Address line 2 (locality, area). |
pincode | string | Optional | 6-digit Indian PIN code | Valid postal code. |
city | string | Optional | Min 1 char | City name. |
district | string | Optional | Min 1 char | District name. |
state | string | Optional | Min 1 char | State name. |
postOffice | string | Optional | Min 1 char | Post office name. |
cityId | number | Optional | Positive integer | Master location city reference. |
postOfficeId | number | Optional | Positive integer | Master location post office reference. |
[!NOTE] At least one address field besides
addressIdmust be provided in the request.
Response (200 OK)¶
{
"status": "success",
"code": 200,
"message": "Address updated successfully",
"data": {
"addressId": "ca_cl0123456789abcdefghijk"
},
"error": null
}
Security & tenant boundaries¶
- Role: Requires
COMPANYrole. - Permission: Requires
company.profile.updatepermission. - Tenant Isolation: Strictly scoped to the authenticated user's active company. Attempting to update an address belonging to another company returns
404 Not Found.
4. Branch operational address management¶
To update or replace a branch's operational physical address, call the dedicated branch endpoint:
PUT /v2/companies/branches/:branchId/address
Authorization: Bearer <company-token>
Content-Type: application/json
{
"line1": "Door 4B, Metro Pillar 520, Palarivattom",
"line2": "Near Pipeline Junction",
"pincode": "682025",
"city": "Kochi",
"district": "Ernakulam",
"state": "Kerala"
}
[!NOTE] Modifying this branch operational address affects only this specific branch. It does not update the company's
registeredAddressorbillingAddress.
5. Read paths¶
5.1 Authenticated company profile (GET /v2/companies/me)¶
{
"status": "success",
"code": 200,
"message": "Company details fetched successfully",
"data": {
"id": "usr_987654321",
"name": "Acme Chits",
"email": "contact@acmechits.com",
"role": "COMPANY",
"company": {
"id": "cmp_123456789",
"branch": "Kochi Head Office",
"registrationNumber": "REG-KL-2024-001",
"yearOfEstablishment": 2018,
"gstin": "32ABCDE1234F1Z5",
"pan": "ABCDE1234F",
"registeredAddress": {
"id": "ca_reg_01",
"type": "REGISTERED",
"line1": "Suite 401, Corporate Plaza",
"line2": "Marine Drive",
"pincode": "682031",
"city": "Kochi",
"district": "Ernakulam",
"state": "Kerala",
"cityId": 101,
"postOfficeId": 210,
"createdAt": "2026-09-20T10:00:00.000Z",
"updatedAt": "2026-09-20T10:00:00.000Z"
},
"billingAddress": {
"id": "ca_bil_02",
"type": "BILLING",
"line1": "Acme Towers, MG Road",
"line2": "Ravipuram",
"pincode": "682016",
"city": "Kochi",
"district": "Ernakulam",
"state": "Kerala",
"cityId": 101,
"postOfficeId": 205,
"createdAt": "2026-09-20T10:00:00.000Z",
"updatedAt": "2026-09-20T10:00:00.000Z"
}
}
}
}
5.2 Company profile for superadmin (GET /v2/admin/companies/:companyId)¶
Returns the full company record including data.company.registeredAddress and data.company.billingAddress. If an address has not been provided yet, its value is null.
5.3 Onboarding progress view (GET /v2/companies/me/new)¶
Returns step-keyed onboarding progress:
{
"status": "success",
"code": 200,
"data": {
"GENERAL_INFO": { ... },
"REGISTRATION_DETAILS": { ... },
"BUSINESS_DETAILS": {
"hasGstin": true,
"gstin": "32ABCDE1234F1Z5",
"billingAddress": {
"id": "ca_bil_02",
"line1": "Acme Towers, MG Road",
"pincode": "682016",
"city": "Kochi",
"district": "Ernakulam",
"state": "Kerala"
},
"registeredSameAsBillingAddress": false,
"registeredAddress": {
"id": "ca_reg_01",
"line1": "Suite 401, Corporate Plaza",
"pincode": "682031",
"city": "Kochi",
"district": "Ernakulam",
"state": "Kerala"
}
}
}
}
5.4 Public programs catalog (GET /v2/public/programs & GET /v2/public/programs/:programId)¶
In the public marketplace catalog, each program exposes the hosting company:
{
"id": "prg_5544332211",
"name": "Secure Monthly Chit 1L",
"totalAmount": 100000,
"subscriptionAmount": 5000,
"durationValue": 20,
"durationType": "MONTHS",
"company": {
"id": "cmp_123456789",
"name": "Acme Chits",
"avatar": null,
"branch": "Palarivattom Branch",
"registeredAddress": {
"line1": "Suite 401, Corporate Plaza",
"line2": "Marine Drive",
"pincode": "682031",
"postOffice": "Marine Drive S.O",
"city": "Kochi",
"district": "Ernakulam",
"state": "Kerala"
}
}
}
[!TIP] Notice how
company.branchcontains the specific operational branch hosting the program (Palarivattom Branch), whilecompany.registeredAddressreflects the company's legal corporate registered address fromcompany_addresses.
6. Frontend TypeScript contracts¶
Copy and reuse these TypeScript definitions in your frontend project:
export type CompanyAddressType = 'REGISTERED' | 'BILLING';
export interface AddressInput {
line1: string;
line2?: string | null;
pincode: string;
postOffice?: string | null;
city: string;
district: string;
state: string;
cityId?: number | null;
postOfficeId?: number | null;
}
export interface CompanyAddressOutput {
id: string;
type?: CompanyAddressType | string | null;
line1: string;
line2?: string | null;
pincode?: string | null;
postOffice?: string | null;
city?: string | null;
district?: string | null;
state?: string | null;
cityId?: number | null;
postOfficeId?: number | null;
createdAt?: string | null;
updatedAt?: string | null;
}
export interface BusinessDetailsFormValues {
hasGstin: boolean;
gstin?: string;
billingAddress: AddressInput;
registeredSameAsBillingAddress: boolean;
registeredAddress?: AddressInput;
}
7. Form implementation pattern (React example)¶
import React, { useState } from 'react';
import type { BusinessDetailsFormValues, AddressInput } from './types';
export const CompanyBusinessDetailsForm: React.FC = () => {
const [sameAsBilling, setSameAsBilling] = useState<boolean>(true);
const [billing, setBilling] = useState<AddressInput>({
line1: '',
line2: '',
pincode: '',
city: '',
district: '',
state: ''
});
const [registered, setRegistered] = useState<AddressInput>({
line1: '',
line2: '',
pincode: '',
city: '',
district: '',
state: ''
});
const [hasGstin, setHasGstin] = useState<boolean>(false);
const [gstin, setGstin] = useState<string>('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Prepare clean payload respecting conditional omission rules
const payload: BusinessDetailsFormValues = {
hasGstin,
...(hasGstin && { gstin: gstin.trim() }),
billingAddress: billing,
registeredSameAsBillingAddress: sameAsBilling,
// OMIT registeredAddress if sameAsBilling is true
...(!sameAsBilling && { registeredAddress: registered })
};
const response = await fetch('/v2/auth/companies/onboarding/steps/3', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
console.error('Submission failed:', errorData);
}
};
return (
<form onSubmit={handleSubmit}>
<h3>Billing Address</h3>
<AddressFields value={billing} onChange={setBilling} />
<label>
<input
type="checkbox"
checked={sameAsBilling}
onChange={e => setSameAsBilling(e.target.checked)}
/>
Registered address is the same as billing address
</label>
{!sameAsBilling && (
<fieldset>
<legend>Registered Address</legend>
<AddressFields value={registered} onChange={setRegistered} />
</fieldset>
)}
<button type="submit">Save and Continue</button>
</form>
);
};