How to Parse a Chinese VAT Invoice (Fapiao) with Code — Fields, Formats, and Pitfalls
If your software touches anyone who buys from, sells to, or operates in China, sooner or later a fapiao (发票) lands in your inbox — usually as a crooked phone photo of a printed A5 sheet, or a PDF that looks like a spread
If your software touches anyone who buys from, sells to, or operates in China, sooner or later a fapiao (发票) lands in your inbox — usually as a crooked phone photo of a printed A5 sheet, or a PDF that looks like a spreadsheet form. Your user wants it in your expense system as structured data. Generic OCR gives you a wall of Chinese text and leaves the hard part to you.
I spent the last few weeks building a parser for these documents. This post is everything I learned about their structure — useful even if you end up writing your own extractor.
What a fapiao actually is
A fapiao is not a receipt your seller prints freely. It is a tax-controlled document issued through China's tax system, and it is the only voucher the tax bureau accepts for VAT deduction and expense reimbursement. The two types you will meet:
- 增值税专用发票 (special VAT invoice) — businesses use it to deduct input VAT. Strictest layout.
- 增值税普通发票 (general VAT invoice) — for everyone else.
Both are increasingly issued as 全电发票 (fully digital e-invoices): no paper, no invoice code, just a 20-digit invoice number. If your parser assumes the classic layout, the digital ones will break it.
The fields, and where they hide
A classic VAT invoice carries:
- 发票代码 (invoice code, 10–12 digits) and 发票号码 (invoice number, 8 digits) — top right. On fully digital invoices, only the 20-digit number exists.
- 开票日期 — issue date, top right under the number.
- 校验码 — a 20-digit check code, used on the tax bureau's verification site.
- 购买方 / 销售方 (buyer / seller) blocks: name plus 纳税人识别号 — the tax ID, for companies the 18-character Unified Social Credit Code.
-
Line items: name (often prefixed with a category like
*信息技术服务*), specification, unit, quantity, unit price, amount. - 税率 / 税额 — VAT rate (13% / 9% / 6% / 3% / 1% depending on category) and tax per line.
-
价税合计 — the tax-inclusive total, printed twice: in digits (小写) and in formal Chinese capital numerals (大写), e.g.
壹仟圆整for ¥1,000.00. - A red 发票专用章 (invoice seal) stamped over the seller block, frequently overlapping the text you need.
Pitfalls that bite every first implementation
- The two totals must agree — use that. 金额 (pre-tax) + 税额 (tax) = 价税合计 (total), and the 大写 amount must equal the 小写 one. These two independent cross-checks catch most OCR digit errors for free. The capital-numeral set (壹贰叁肆伍陆柒捌玖拾佰仟圆角分整) is small; parsing it is an afternoon's work and pays for itself immediately.
- Red-ink invoices exist. A 红字发票 is a credit-note invoice with negative amounts, issued to void or correct a blue one. If your pipeline sums invoices, treat red ones as first-class citizens, not parse errors.
- The "same" family isn't the same document. Train e-tickets (铁路电子客票), flight itineraries (航空运输电子客票行程单) and taxi receipts share the reimbursement workflow but have completely different fields (passenger ID, train number, fare class…). Auto-detect the document type first, extract second.
- The seal is part of the image. The red stamp sits on top of seller name and tax ID. OCR engines vary wildly here; expect to try preprocessing (and to distrust low-confidence characters in the stamped zone).
- The QR code is a cheat code. The QR printed on the invoice encodes the code, number, pre-tax amount, date and check code. When it's legible, decode it and use it to verify (or replace) your OCR'd key fields.
- Tax IDs are validatable. The 18-char Unified Social Credit Code has a check digit (GB 32100). Validate before you store — a misread character otherwise poisons your accounting data silently.
The approach that worked for me
OCR for the text layer → anchor-based field extraction (the labels 名称 / 税号 / 价税合计 are stable even when layouts shift) → cross-validation of every numeric field (line sums, totals, 大写 vs 小写, check digits) → only then emit JSON:
{
"document_type": "vat_invoice",
"invoice_code": "044002100111",
"invoice_number": "38291047",
"issue_date": "2026-10-28",
"buyer": { "name": "…", "tax_id": "91310000XXXXXXXXXX" },
"seller": { "name": "…", "tax_id": "91440101XXXXXXXXXX" },
"items": [
{ "name": "*信息技术服务*软件开发费", "amount": 943.40, "tax_rate": 0.06, "tax": 56.60 }
],
"amount_excl_tax": 943.40,
"tax_total": 56.60,
"total_incl_tax": 1000.00
}
Try it / skip the build
I packaged this parser as a small API (China Invoice OCR — Fapiao Parser on RapidAPI — it also auto-detects train tickets, flight itineraries and taxi receipts). If you just want to see the extraction on a real document, there's a free web demo: upload an invoice image, get the field table back, no signup.
Full disclosure: that's my own API — but honestly, if your volume is low, the DIY route above is completely viable, and the cross-validation tricks matter more than which OCR engine you pick.
What did I miss? If you've fought fapiao (or the fully digital formats) in production, I'd like to hear which pitfall got you.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.