Skip to content

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

  1. 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.
  2. 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.
  3. Subscriber addresses: the addresses table holds only subscriber addresses (SUBSCRIBER_PERMANENT, SUBSCRIBER_PRESENT). Company users never have records in the addresses table.

2. Address routes

  1. PATCH /v2/companies/me/addresses updates corporate addresses in company_addresses (REGISTERED or BILLING) by passing addressId. Branch operational addresses are updated per branch via PUT /v2/companies/branches/:branchId/address.
  2. POST /v1/address accepts only SUBSCRIBER_PERMANENT and SUBSCRIBER_PRESENT.
  3. Company addresses use the dedicated enum CompanyAddressType (REGISTERED | BILLING).

3. Write paths

3.1 Superadmin company provisioning

POST /v2/admin/companies
Authorization: Bearer <superadmin-token>
Content-Type: application/json

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

  • billingAddress is mandatory.
  • registeredSameAsBillingAddress defaults to false.
  • When registeredSameAsBillingAddress === true:
  • The form must omit registeredAddress (or send undefined). Sending an object will trigger a validation error: "Registered address must be omitted when it matches the billing address."
  • The backend automatically copies billingAddress into company_addresses with type: 'REGISTERED'.
  • When registeredSameAsBillingAddress === false:
  • registeredAddress is 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 === true and gstin is missing: returns "GSTIN is required if you have one."
  • If registeredSameAsBillingAddress === false (or omitted) and registeredAddress is missing: returns "Registered address is required if it is not the same as billing address".
  • If registeredSameAsBillingAddress === true and registeredAddress is provided: returns "Registered address should not be provided when it is same as billing address".
  • Both billingAddress and registeredAddress require positive integers for cityId and postOfficeId.

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

  1. Updates the headquarters branch's operational physical address in branch_addresses using registeredAddress (or billingAddress if matching).
  2. Upserts company_addresses with type: 'REGISTERED'.
  3. Upserts company_addresses with type: 'BILLING'.
  4. Updates company record with onboardingStep = 'BUSINESS_DETAILS' and advances nextStep to '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 addressId must 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 COMPANY role.
  • Permission: Requires company.profile.update permission.
  • 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 registeredAddress or billingAddress.


5. Read paths

5.1 Authenticated company profile (GET /v2/companies/me)

GET /v2/companies/me
Authorization: Bearer <company-token>
{
  "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)

GET /v2/admin/companies/:companyId
Authorization: Bearer <superadmin-token>

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)

GET /v2/companies/me/new
Authorization: Bearer <company-token>

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.branch contains the specific operational branch hosting the program (Palarivattom Branch), while company.registeredAddress reflects the company's legal corporate registered address from company_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>
  );
};