---
title: "India GST invoices"
description: "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."
canonical_url: "https://twominutebilling.com/help/developers/recipes/india-gst-invoices"
---
# 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](https://twominutebilling.com/help/developers/getting-started/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.

```ts [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

<code-group>

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

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Sahyadri Softworks", "default_currency": "INR" }'

export BIZ=biz_…
```

</code-group>

## 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.

<code-group>

```ts [TypeScript]
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",
});
```

```bash [curl]
curl -X PATCH $TMBILLING_URL/v1/businesses/$BIZ \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "address": { "line1": "10 Residency Road", "city": "Bengaluru", "state_code": "KA", "postal_code": "560025", "country": "IN" },
    "tax_registration_number": "29AAAAA0000A1Z5"
  }'
```

</code-group>

<note title="Several businesses under one GSTIN">

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.

</note>

## 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.

<code-group>

```ts [TypeScript]
const region = await tmb.taxRegions.create(biz.id, {
  country: "IN",
  regime: "gst",
  home_state_code: "KA",
});
```

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/tax_regions \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "country": "IN",
    "regime": "gst",
    "home_state_code": "KA"
  }'

export TXG=txg_…
```

</code-group>

```json [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.

<code-group>

```ts [TypeScript]
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" },
  ],
});
```

```bash [curl]
curl -X PUT $TMBILLING_URL/v1/businesses/$BIZ/tax_regions/$TXG/components \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "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" }
    ]
  }'
```

</code-group>

```json [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.

<code-group>

```ts [TypeScript]
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",
});
```

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/products \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Pro", "tax_code": "998314" }'

curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/prices \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "product": "pro",
    "name": "Pro monthly",
    "model": "flat",
    "currency": "INR",
    "amount_minor": 249900,
    "interval": "month",
    "tax_behavior": "exclusive"
  }'
```

</code-group>

`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:

<code-group>

```ts [TypeScript]
type Customer = { id: string };

const sameState = await api<Customer>("POST", `/v1/businesses/${biz.id}/customers`, {
  email: "accounts@example.com",
  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: "billing@example.org",
  company: "Example Traders LLP",
  currency: "INR",
  address: { line1: "2 Marine Drive", city: "Mumbai", state_code: "MH", postal_code: "400002", country: "IN" },
});
```

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "accounts@example.com",
    "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" }
  }'

export SAME=cus_…

curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "billing@example.org",
    "company": "Example Traders LLP",
    "currency": "INR",
    "address": { "line1": "2 Marine Drive", "city": "Mumbai", "state_code": "MH", "postal_code": "400002", "country": "IN" }
  }'

export OTHER=cus_…
```

</code-group>

## 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.

<code-group>

```ts [TypeScript]
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 }],
});
```

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers/$SAME/subscription_estimate \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "items": [{ "price": "pro-monthly" }] }'

curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers/$OTHER/subscription_estimate \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "items": [{ "price": "pro-monthly" }] }'
```

</code-group>

```json [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
}
```

```json [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.

<code-group>

```ts [TypeScript]
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 });
```

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers/$SAME/payment_methods \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "token": "4242424242424242" }'

curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/customers/$SAME/subscriptions \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "items": [{ "price": "pro-monthly" }] }'

curl "$TMBILLING_URL/v1/businesses/$BIZ/invoices?customer=$SAME" \
  -H "Authorization: Bearer $TMBILLING_API_KEY"
```

</code-group>

```json [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

<table>
<thead>
  <tr>
    <th>
      What you see
    </th>
    
    <th>
      Why
    </th>
    
    <th>
      What to do
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      The estimate has no <code>
        tax_lines
      </code>
    </td>
    
    <td>
      The customer has no <code>
        country
      </code>
      
      , or no enabled tax region matches it.
    </td>
    
    <td>
      Set the customer's billing address with <code>
        country: "IN"
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      IGST on a sale inside your state
    </td>
    
    <td>
      The customer's <code>
        state_code
      </code>
      
       differs from <code>
        home_state_code
      </code>
      
      , or is missing.
    </td>
    
    <td>
      Set the customer's billing <code>
        state_code
      </code>
      
      . A customer with no state is taxed as inter-state.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        state_code takes the code, not the name. Use "KA" for Karnataka
      </code>
    </td>
    
    <td>
      You sent the state name in <code>
        state_code
      </code>
      
      .
    </td>
    
    <td>
      Send the code. Put the name in <code>
        state
      </code>
      
       if you want it printed.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        A flat region can have only 'both' components…
      </code>
    </td>
    
    <td>
      The region's <code>
        regime
      </code>
      
       is not <code>
        gst
      </code>
      
      , so <code>
        intra
      </code>
      
       and <code>
        inter
      </code>
      
       cannot apply.
    </td>
    
    <td>
      Create the region with <code>
        regime: "gst"
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        component names must be unique within a state scope
      </code>
    </td>
    
    <td>
      Two components share a name.
    </td>
    
    <td>
      Give each component its own name.
    </td>
  </tr>
</tbody>
</table>

## Related

- [GST billing software for SaaS in India](https://twominutebilling.com/solutions/india)
- [Set up tax in the dashboard](https://twominutebilling.com/help/guides/set-up-tax)
- [How tax is calculated](https://twominutebilling.com/help/developers/concepts/tax)


## Sitemap

See the full [sitemap](https://twominutebilling.com/sitemap.md) for all pages.
