Recipes

India GST invoices

Charge GST from an Indian business: CGST and SGST inside your state, IGST across states, with your GSTIN and the HSN or SAC code on every invoice you issue.

Bill Indian customers with GST worked out per invoice. tmbilling compares the state you supply from with the customer's state (the place of supply). The same state gets CGST and SGST. A different state gets IGST. Your GSTIN, the customer's GSTIN and the HSN or SAC code print on the invoice.

Before you begin

You need a test API key and the client from the Quickstart. Rates and states below are examples: use the rates that apply to what you sell. Responses are shortened to the fields that matter here.

setup.ts
import { Tmbilling } from "@two-minute-billing/sdk";

const tmb = new Tmbilling({
  apiKey: process.env.TMBILLING_API_KEY!,
  baseUrl: process.env.TMBILLING_URL,
});

// The client does not type every tax field yet. This helper sends any request with your key.
async function api<T>(method: string, path: string, body?: unknown): Promise<T> {
  const res = await fetch(`${process.env.TMBILLING_URL}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${process.env.TMBILLING_API_KEY}`,
      "Idempotency-Key": crypto.randomUUID(),
      ...(body === undefined ? {} : { "Content-Type": "application/json" }),
    },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
  return (await res.json()) as T;
}

Create an INR business

const biz = await tmb.businesses.create({ name: "Sahyadri Softworks", default_currency: "INR" });

Put your GSTIN on the business

The invoice prints the seller's name, address and GSTIN from the business, and freezes them when the invoice is issued. Set the business address in India and its GSTIN. The label "GSTIN" comes from the address country.

await tmb.businesses.update(biz.id, {
  address: { line1: "10 Residency Road", city: "Bengaluru", state_code: "KA", postal_code: "560025", country: "IN" },
  tax_registration_number: "29AAAAA0000A1Z5",
});
If one registered company bills through several businesses, set the organization's legal name, address and GSTIN, and set invoice_issuer to organization on each business. The invoice then names the company as the seller.

Turn on GST for India

A tax region makes one country taxable. Set regime to gst and home_state_code to the state you supply from, as an ISO 3166-2 code without the country prefix (KA is Karnataka). The home state decides intra-state or inter-state for every sale.

const region = await tmb.taxRegions.create(biz.id, {
  country: "IN",
  regime: "gst",
  home_state_code: "KA",
});
Response
{
  "object": "tax_region",
  "id": "txg_XXXXXXXXXXXXXXXXXXXXXX01",
  "country": "IN",
  "regime": "gst",
  "price_type": "exclusive",
  "home_state_code": "KA",
  "enabled": true,
  "components": []
}

Set the GST rates

The region's tax is a list of components. applies_to says when each one applies: intra for a sale inside your home state, inter for a sale to another state. Rates are basis points, so 900 is 9%. Set IGST to the sum of CGST and SGST.

await tmb.taxRegions.setComponents(biz.id, region.id, {
  components: [
    { name: "CGST", rate_bps: 900, applies_to: "intra" },
    { name: "SGST/UTGST", rate_bps: 900, applies_to: "intra" },
    { name: "IGST", rate_bps: 1800, applies_to: "inter" },
  ],
});
Response
{
  "object": "list",
  "data": [
    {
      "object": "tax_rate_component",
      "id": "txc_XXXXXXXXXXXXXXXXXXXXXX01",
      "name": "CGST",
      "rate_bps": 900,
      "applies_to": "intra",
      "state_code": null
    },
    {
      "object": "tax_rate_component",
      "id": "txc_XXXXXXXXXXXXXXXXXXXXXX02",
      "name": "SGST/UTGST",
      "rate_bps": 900,
      "applies_to": "intra",
      "state_code": null
    },
    {
      "object": "tax_rate_component",
      "id": "txc_XXXXXXXXXXXXXXXXXXXXXX03",
      "name": "IGST",
      "rate_bps": 1800,
      "applies_to": "inter",
      "state_code": null
    }
  ]
}

This call replaces the whole list. To give one state a different rate, send the same call with state_code: that list then wins for sales into that state.

Add the HSN or SAC code

tax_code holds the HSN or SAC code that prints on each invoice line. tmbilling reads it from the price first, then the product, then the business's default_tax_code. Set it once on the product.

const product = await api<{ id: string }>("POST", `/v1/businesses/${biz.id}/products`, {
  name: "Pro",
  tax_code: "998314",
});

const price = await tmb.prices.create(biz.id, {
  product: product.id,
  name: "Pro monthly",
  model: "flat",
  currency: "INR",
  amount_minor: 249900,
  interval: "month",
  tax_behavior: "exclusive",
});

tax_behavior: "exclusive" adds GST on top of ₹2,499.00. With inclusive, ₹2,499.00 is the total and tmbilling takes the tax out of it, using the one combined rate.

Create customers with their state

The customer's billing state_code is the place of supply. Their own GSTIN goes in tax_registration_number, and prints on the invoice. Create one customer in your home state and one in another state:

type Customer = { id: string };

const sameState = await api<Customer>("POST", `/v1/businesses/${biz.id}/customers`, {
  email: "[email protected]",
  company: "Example Software Pvt Ltd",
  currency: "INR",
  tax_registration_number: "29BBBBB1111B1Z5",
  address: { line1: "1 MG Road", city: "Bengaluru", state_code: "KA", postal_code: "560001", country: "IN" },
});

const otherState = await api<Customer>("POST", `/v1/businesses/${biz.id}/customers`, {
  email: "[email protected]",
  company: "Example Traders LLP",
  currency: "INR",
  address: { line1: "2 Marine Drive", city: "Mumbai", state_code: "MH", postal_code: "400002", country: "IN" },
});

Preview the tax

An estimate shows the first invoice without creating anything. The same-state customer gets CGST and SGST. The other-state customer gets IGST.

const intra = await tmb.subscriptions.estimateNew(biz.id, sameState.id, {
  items: [{ price: price.id }],
});
const inter = await tmb.subscriptions.estimateNew(biz.id, otherState.id, {
  items: [{ price: price.id }],
});
Response (same state)
{
  "object": "estimate",
  "currency": "INR",
  "subtotal_minor": 249900,
  "tax_minor": 44982,
  "tax_lines": [
    {
      "label": "CGST (9.00%)",
      "amount_minor": 22491
    },
    {
      "label": "SGST/UTGST (9.00%)",
      "amount_minor": 22491
    }
  ],
  "total_minor": 294882,
  "amount_due_minor": 294882
}
Response (other state)
{
  "object": "estimate",
  "currency": "INR",
  "subtotal_minor": 249900,
  "tax_minor": 44982,
  "tax_lines": [
    {
      "label": "IGST (18.00%)",
      "amount_minor": 44982
    }
  ],
  "total_minor": 294882,
  "amount_due_minor": 294882
}

Bill the customer

Save a test card and subscribe the same-state customer. The first invoice is raised and paid.

await tmb.paymentMethods.create(biz.id, sameState.id, { token: "4242424242424242" });
const sub = await tmb.subscriptions.create(biz.id, sameState.id, {
  items: [{ price: price.id }],
});
const invoices = await tmb.invoices.list(biz.id, { customer: sameState.id });
Response
{
  "object": "list",
  "data": [
    {
      "object": "invoice",
      "id": "inv_XXXXXXXXXXXXXXXXXXXXXX01",
      "number": "INV-1",
      "status": "paid",
      "currency": "INR",
      "subtotal_minor": 249900,
      "total_minor": 294882,
      "seller_state_code": "KA",
      "place_of_supply_code": "KA",
      "customer_tax_id": "29BBBBB1111B1Z5",
      "tax_origin": {
        "country": "IN",
        "registration_number": "29AAAAA0000A1Z5"
      },
      "price_type": "tax_exclusive"
    }
  ]
}

Check it worked

  • seller_state_code and place_of_supply_code on the invoice are the two states tmbilling compared. Equal means CGST and SGST. Different means IGST.
  • tax_origin.registration_number is your GSTIN and customer_tax_id is the buyer's, both frozen at issue. Changing the business later does not rewrite issued invoices.
  • GET /v1/businesses/{biz}/invoices/{inv}/pdf returns the tax invoice, with an HSN/SAC column and one row per tax component.

Common errors

What you seeWhyWhat to do
The estimate has no tax_linesThe customer has no country, or no enabled tax region matches it.Set the customer's billing address with country: "IN".
IGST on a sale inside your stateThe customer's state_code differs from home_state_code, or is missing.Set the customer's billing state_code. A customer with no state is taxed as inter-state.
state_code takes the code, not the name. Use "KA" for KarnatakaYou sent the state name in state_code.Send the code. Put the name in state if you want it printed.
A flat region can have only 'both' components…The region's regime is not gst, so intra and inter cannot apply.Create the region with regime: "gst".
component names must be unique within a state scopeTwo components share a name.Give each component its own name.
Copyright © 2026