You have a phone full of photos: a fuel bill, a courier invoice, a stationery receipt, a cab invoice. Month end comes, and someone has to turn them into rows your accountant can use. AI receipt and invoice OCR can do the typing in minutes. The catch is that a model reads a blurred "8" as a "3" without blinking, and nothing in the sheet will tell you.
So this post is about the part after the reading. You get an extraction prompt, a small Python checker for Indian GST fields with real output, and a review routine. The checker runs on five invented receipts I ran on 2026-10-31. I did not run any live OCR or model call, so the extraction step is a stub and the prompt is an untested template. Nothing here is tax advice.
If you want the generic version without the India fields, see AI invoice data extraction into Google Sheets. For the heavier PDF and scanned-document route, see the invoice extraction agent. This post adds the GST-specific checks and the receipt problem (receipts are messier than invoices).
What does the workflow look like?
Four stages, and only the first one uses a model.
- Extract. A vision-capable model reads the photo or PDF and returns fixed fields.
- Land. Rows go into an
extractedtab exactly as returned. - Check. Plain code marks each row
READY_FOR_HUMANorNEEDS_REVIEW, with the reason. - Review. A person opens the original image for every flagged row, plus a few unflagged ones.
Your CA files and decides. The sheet never does. Notice the good verdict is not "approved". It only means a human may look next.
Which fields should you extract from a GST invoice?
Rule 46 of the CGST Rules, 2017 lists what a tax invoice must carry. I could not open the legal text on the CBIC site from my environment (the page I tried had no rule text), so I checked a search summary on 2026-10-31 and several compliance blogs that agree on the main items: supplier and recipient details with GSTIN, a consecutive invoice number and date, HSN or SAC, description, quantity, taxable value, rate and amount of tax, and place of supply for inter-State supplies. Those sources disagree on whether the count is 16 or 17 items and none of them is the legal text. Read the current rule on the CBIC site, or ask your CA, before you rely on my list.
For a spreadsheet you don't need every item. These are the fields the script below uses:
| Field | Why it earns a column |
|---|---|
vendor | Human-readable key for review |
supplier_gstin | Format and check-character test, state code |
buyer_gstin | Is the bill actually made out to your business? |
number, date | Duplicate test, financial-year test |
taxable, rate | Recompute the tax |
cgst, sgst, igst | Tax type must fit the state pair |
total | Parts must add up |
Add hsn_sac and place_of_supply if your CA wants them. I left them out because the checks stay clear with fewer columns.
What does the extraction prompt look like?
Untested template. Run it on five of your own documents and compare field by field. If your tool supports structured outputs, use them; see Claude structured outputs with JSON and Pydantic and the vision API guide for sending images.
Read the attached bill (photo or PDF) and return JSON only.
Rules:
- If a field is not visible, use null. Never guess or calculate a missing value.
- Copy numbers exactly as printed. Amounts as strings with two decimals, no symbols.
- Dates as YYYY-MM-DD. If the year is ambiguous, return the date as printed and set date_ambiguous to true.
- GSTINs: copy character by character, uppercase. Do not "fix" one that looks wrong.
- If the bill shows no tax breakup, leave cgst, sgst, igst and rate null. Do not infer them.
Fields: vendor, supplier_gstin, buyer_gstin, number, date, date_ambiguous,
taxable, rate, cgst, sgst, igst, total, notes (anything unreadable, in one sentence).
Two lines do most of the work. "Do not fix a GSTIN that looks wrong" keeps a misread visible, where a helpful model would quietly repair it into a valid-looking wrong number. "Never calculate a missing value" stops the model from filling igst by arithmetic so that the row passes your own checks.
What does the checker script catch?
The script below is plain Python with no dependencies. stub_extract stands in for the model call and returns five invented receipts. The GSTINs are invented too, built with a computed check character, so a real registration could coincidentally match. They are not anyone's real numbers.
About the check character: the last character of a GSTIN is a check value, and the script uses the commonly published mod-36 method (alternating weights of 1 and 2, then adding quotient and remainder). I did not find that algorithm on an official GST page, so treat it as a typo detector. It tells you a string is well-formed, not that the registration exists.
import csv, re, sys
from datetime import date
from decimal import Decimal
CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
GSTIN_RE = re.compile(r"^\d{2}[A-Z]{5}\d{4}[A-Z][1-9A-Z]Z[0-9A-Z]$")
STATES = {"07": "Delhi", "27": "Maharashtra", "29": "Karnataka", "33": "Tamil Nadu"}
# Demo values. Fill from the current GST rate notification, which I did not verify here.
ALLOWED_RATES = {Decimal("0"), Decimal("5"), Decimal("18")}
FY_START, FY_END = date(2026, 4, 1), date(2027, 3, 31)
def gstin_check_char(first14):
total, factor = 0, 1
for ch in first14:
p = CHARS.index(ch) * factor
total += p // 36 + p % 36
factor = 2 if factor == 1 else 1
return CHARS[(36 - total % 36) % 36]
def gstin_problems(g):
if not g:
return ["missing"]
g = g.strip().upper()
if not GSTIN_RE.match(g):
return ["bad format"]
out = []
if g[:2] not in STATES:
out.append("state code not in my demo table")
if gstin_check_char(g[:14]) != g[14]:
out.append("checksum mismatch")
return out
def D(x):
return Decimal(x) if x not in ("", None) else None
def check(r, seen):
flags = []
sg, bg = r["supplier_gstin"], r["buyer_gstin"]
if not r["number"]:
flags.append("NO_INVOICE_NUMBER")
if sg:
flags += ["SUPPLIER_GSTIN:" + p for p in gstin_problems(sg)]
else:
flags.append("NO_SUPPLIER_GSTIN")
if bg:
flags += ["BUYER_GSTIN:" + p for p in gstin_problems(bg)]
else:
flags.append("NO_BUYER_GSTIN")
tx, rate, c, s, i, tot = (D(r[k]) for k in ("taxable", "rate", "cgst", "sgst", "igst", "total"))
if None not in (tx, rate, c, s, i, tot):
if rate not in ALLOWED_RATES:
flags.append("RATE_NOT_IN_ALLOWED_LIST")
if tx + c + s + i != tot:
flags.append("TOTAL_MISMATCH")
if abs((c + s + i) - tx * rate / 100) > Decimal("0.02"):
flags.append("TAX_NOT_RATE_TIMES_TAXABLE")
if sg and bg:
intra = sg[:2] == bg[:2]
if intra and i != 0:
flags.append("IGST_ON_SAME_STATE")
if not intra and (c != 0 or s != 0):
flags.append("CGST_SGST_ON_INTERSTATE")
if intra and c != s:
flags.append("CGST_NE_SGST")
elif tot is not None and sg:
flags.append("TAX_FIELDS_MISSING")
y, m, d = (int(x) for x in r["date"].split("-"))
if not FY_START <= date(y, m, d) <= FY_END:
flags.append("DATE_OUTSIDE_FY")
key = (sg, r["number"])
if r["number"] and key in seen:
flags.append("DUPLICATE_OF_" + seen[key])
elif r["number"]:
seen[key] = r["_id"]
return flags
The remaining part of the file is the stub with five receipts and a loop that prints a verdict per receipt and writes a CSV. Here is what it printed when I ran it on 2026-10-31:
29ABCDE1234F1ZW -> ok
07ABCDE1234F1Z2 -> ok
27ABCDE1234F1Z0 -> ok
27ABCDE1234F1Z1 -> ['checksum mismatch']
r1 Sunrise Stationers READY_FOR_HUMAN
r2 Delhi Cabs NEEDS_REVIEW TAX_NOT_RATE_TIMES_TAXABLE;CGST_SGST_ON_INTERSTATE
r3 Chai Point Cafe NEEDS_REVIEW NO_INVOICE_NUMBER;NO_SUPPLIER_GSTIN;NO_BUYER_GSTIN
r4 Sunrise Stationers NEEDS_REVIEW DUPLICATE_OF_r1
r5 Pune Print Works NEEDS_REVIEW SUPPLIER_GSTIN:checksum mismatch;CGST_SGST_ON_INTERSTATE;DATE_OUTSIDE_FY
I built each bad receipt to fail in a specific way, so this output shows that the rules fire, not how good any model is. Here's what each flag means in practice.
- r2, the cab invoice. A Delhi supplier billing a Karnataka buyer shows CGST and SGST of 25.00 each on a 500.00 taxable value at 5 percent. Two things are off: the tax type should normally be IGST for an inter-state supply, and 50.00 is double the expected 25.00. A model that read the tax once as "CGST" and once as "SGST" from a single line would do exactly this.
- r3, the cafe slip. No invoice number, no GSTINs. This is a plain bill. It may still be a legitimate expense, but there is nothing to match on, so it lands in review. Whether it supports any tax credit is a question for your CA, not for the sheet.
- r4, the repeat. The same supplier and invoice number as r1. Photos get taken twice, and emailed copies get saved again.
- r5, the print shop. A one-character GSTIN error, a date a year in the future (a misread "6" as "7"), and CGST and SGST on a Maharashtra to Karnataka pair.
One more design choice: the checker returns every reason, not the first one. A reviewer who sees three flags knows to distrust the whole row.
How do you get the rows into Google Sheets?
Keep it boring. The script writes out.csv; in Google Sheets use File, Import, and pick "Append to current sheet" on the extracted tab. If you'd rather call a model from inside the sheet, Claude from Google Sheets with Apps Script shows a menu-driven pattern with the key stored in Script Properties. I did not combine it with the checker.
Two formulas help the reviewer. In a review tab, =FILTER(extracted!A:G, extracted!F:F="NEEDS_REVIEW") lists the rows to open. Conditional formatting on the flags column highlights anything containing GSTIN. Neither is needed; they just save clicks.
What should a person review?
A short checklist, in order:
- Open the original image for every
NEEDS_REVIEWrow. Fix the sheet, never the flag. - Spot-check at least five
READY_FOR_HUMANrows against the image. If you find one error in five, widen the sample until you trust the pipeline or stop using it. - Look up each new supplier GSTIN on the GST portal once, and keep a
vendorstab of the ones you've confirmed. The checker's check character doesn't replace this. - Send the CA the sheet and the images. Don't summarise tax treatment yourself.
Privacy matters here too. Bills carry names, phone numbers and sometimes card fragments. Read what not to paste into ChatGPT and, if you handle Aadhaar or PAN numbers on documents, masking them before an LLM call. Your CA may also want to see month-end prompts for accountants so you hand over rows in the shape they expect.
What can go wrong?
- Rate lists go stale. The
ALLOWED_RATESset is a demo. GST rates and slabs change by notification, so source your list from the current notification or your CA, and date it in the code. - Place of supply is not always the registration state. The state-pair check is a flag, not a verdict. Some supplies are taxed by place of supply rules that differ from where the supplier is registered.
- Rounding. Invoices round per line, per invoice, or not at all. The 0.02 tolerance is a guess. Widen it if you see false flags, and write down why.
- Handwritten and thermal receipts fade. Photograph them the day you get them.
- Same-number, different-year invoices. Series restart in April for many businesses, so the duplicate key might need the financial year. Mine doesn't have it.
What would I not automate?
Filing, credit decisions, anything that touches the GST portal on your behalf, and any message to a vendor. Extraction is a clerical step. The sheet is a worksheet your CA can audit.



