DEV Community

China Sourcing Guard
China Sourcing Guard

Posted on Originally published at chinasourcingguard.hashnode.dev

Validating Chinese Business License Codes (USCC) in TypeScript

If you build anything that onboards Chinese companies (B2B marketplaces, KYB/KYC flows, procurement tools, invoicing, supplier portals), sooner or later a form asks for a 统一社会信用代码, the Unified Social Credit Code (USCC). It's China's equivalent of a company registration number + tax ID in one.

It is also one of those identifiers that people paste with spaces, lowercase letters, a stray O instead of 0, or simply make up. The good news: it has a checksum, so you can reject a lot of garbage at the form layer before it hits your database or a manual review queue.

This post covers:

  1. The structure of the 18-character code
  2. The GB 32100-2015 check-digit algorithm (with a from-scratch TypeScript implementation)
  3. Using the open-source uscc-validator library to validate and decode codes
  4. Wiring it into Zod / a form
  5. What a checksum can't tell you

1. Anatomy of a USCC

Since 2015 every legal entity in mainland China gets one, defined by national standard GB 32100-2015. It's printed on the business license (营业执照).

9 1 440300 MA5XXXXXX X
│ │   │        │      └─ 18: check digit (mod 31)
│ │   │        └──────── 9–17: organization code (GB 11714)
│ │   └───────────────── 3–8: administrative division (GB/T 2260), e.g. 440300 = Shenzhen
│ └───────────────────── 2: organization type (under "9": 1 = enterprise, 2 = individual business)
└─────────────────────── 1: registration authority (9 = market regulation / SAMR)
Enter fullscreen mode Exit fullscreen mode

The alphabet is 0-9 plus uppercase letters excluding I, O, S, V, Z, so 31 symbols in total:

export const USCC_CHARSET = '0123456789ABCDEFGHJKLMNPQRTUWXY';
Enter fullscreen mode Exit fullscreen mode

A neat practical detail: the first two characters are business-relevant. 91… is a company; 92… is an 个体工商户 (individual / sole-trader business). If your users are buyers vetting suppliers, surfacing "this is a sole trader" next to a supplier claiming 500 employees is genuinely useful UX.

2. The check digit, from scratch

Each of the first 17 characters is mapped to its index in the charset, multiplied by a fixed positional weight, summed, and reduced mod 31.

const CHARSET = '0123456789ABCDEFGHJKLMNPQRTUWXY';
const WEIGHTS = [1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28];

export function checkDigit(first17: string): string | null {
  if (first17.length !== 17) return null;
  let sum = 0;
  for (let i = 0; i < 17; i++) {
    const v = CHARSET.indexOf(first17[i]);
    if (v < 0) return null;            // illegal character (I, O, S, V, Z, lowercase…)
    sum += v * WEIGHTS[i];
  }
  const c = 31 - (sum % 31);
  return CHARSET[c === 31 ? 0 : c];
}

export function isValidUscc(code: string): boolean {
  return /^[0-9A-HJ-NPQRTUWXY]{18}$/.test(code) && checkDigit(code.slice(0, 17)) === code[17];
}
Enter fullscreen mode Exit fullscreen mode

(The weights are 3^i mod 31, which is why they look random.)

That's ~20 lines and it works. But in production you quickly want more: input normalization, human-readable errors, decoding the authority/type/region, and the secondary GB 11714 org-code check. That's what the library is for.

3. Using uscc-validator

uscc-validator is MIT-licensed, zero-dependency, pure functions, no network calls, and runs in Node and the browser.

# install straight from GitHub
npm install github:steplead/uscc-validator
Enter fullscreen mode Exit fullscreen mode
import { validateUscc, computeUsccCheckDigit } from 'uscc-validator';

const r = validateUscc('91110000802100433B');
// {
//   formatOk: true,
//   checkDigitOk: true,
//   authority: 'Market regulation authority (SAMR / AIC) — commercial companies',
//   orgType: 'Enterprise (company)',
//   isCompany: true,
//   isSoleTrader: false,
//   regionCode: '110000',
//   province: 'Beijing',
//   city: 'Beijing',
//   orgCode: '802100433',
//   errors: []
// }

validateUscc('914403001922038217').errors;
// ['Check digit mismatch: the 18th character should be "6" but is "7". ...']

computeUsccCheckDigit('91110000802100433'); // 'B'
Enter fullscreen mode Exit fullscreen mode

Things it handles that the naive version doesn't:

  • Normalization: strips spaces/dashes, upper-cases, and fixes the classic O→0 and I→1 typos. Users copy codes from photos of paper licenses and OCR output, so this matters.
  • Actionable errors: "the 18th character should be 6" is something a user can act on, and it often reveals a single-character transcription mistake.
  • Decoding: authority, org type, isCompany / isSoleTrader, province/city for common manufacturing hubs.
  • orgCodeCheckOk: the secondary GB 11714 check on positions 9–17 (only meaningful for converted legacy codes; newer MA… codes may not satisfy it, so treat it as a soft signal).

4. Plugging it into a form schema

With Zod:

import { z } from 'zod';
import { validateUscc } from 'uscc-validator';

export const SupplierSchema = z.object({
  legalNameZh: z.string().min(2),
  uscc: z
    .string()
    .transform((s) => validateUscc(s))
    .superRefine((r, ctx) => {
      if (!r.formatOk || !r.checkDigitOk) {
        ctx.addIssue({ code: z.ZodIssueCode.custom, message: r.errors[0] ?? 'Invalid USCC' });
      }
    })
    .transform((r) => ({ code: r.normalized, isSoleTrader: r.isSoleTrader, province: r.province })),
});
Enter fullscreen mode Exit fullscreen mode

Store the normalized code, not the raw input, so de-duplication and lookups work.

5. What a valid checksum does not mean

This is the part worth putting in your UI copy:

A correct check digit means the code is well-formed. It does not mean the company exists.

Anyone can generate a checksum-valid code in one line. The library can't tell you the company name, legal representative, status (active / revoked / deregistered) or business scope. For that you need the official registry, GSXT (国家企业信用信息公示系统, gsxt.gov.cn), which has no public API, is Chinese-only, and sits behind a captcha. So a sensible pipeline looks like this:

  1. Offline validation (this library) at input time: instant, free, catches typos and fabricated codes.
  2. Registry confirmation (manual or via a licensed data provider): name, status, scope.
  3. Business-rule checks: does the Chinese legal name on the registry match the contract and the bank beneficiary?

If you want to see the same logic in a UI before integrating it, there's a browser-based China business license checker built on it.

Wrap-up

  • USCC = 18 chars, 31-symbol alphabet, weighted mod-31 check digit.
  • Validate and normalize at the edge; store the normalized code.
  • Surface decoded hints (sole trader vs. company, region) because they're cheap and useful.
  • Never present "valid checksum" as "verified company".

Issues and PRs are welcome on GitHub, especially for the region table, which covers provinces plus major manufacturing prefectures but not the full GB/T 2260 list yet.

Originally published on Hashnode, where it's written for buyers vetting suppliers rather than developers.

Top comments (0)