DEV Community

Rick
Rick

Posted on

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

  1. 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.
  2. 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.
  3. 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.
  4. 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).
  5. 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.
  6. 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
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)