Trade compliance data · implementation
The UFLPA Entity List as data: 205 rows, 187 entities
DHS publishes the Uyghur Forced Labor Prevention Act Entity List as a web page, not a file. A program that screens supplier names against it has to turn three HTML tables into a list, and several things about those tables change the answer. Each one below was measured on the live page on , with the script and its output at the end. For what a match means and what a name screen cannot tell you, see How to check a supplier against the UFLPA Entity List.
Where the data lives
One page, dhs.gov/uflpa-entity-list, with 0 links to a CSV, JSON or spreadsheet file. It holds one table per category of the Act, each under a heading and a link to its section. Each table has two columns, the entity's name and an effective date.
| section | rows | heading, as printed |
|---|---|---|
| 2(d)(2)(B)(i) | 9 | A list of entities in Xinjiang that mine, produce, or manufacture wholly or in part any goods, wares, articles and merchandise with forced labor |
| 2(d)(2)(B)(ii) | 47 | A list of entities working with the government of Xinjiang to recruit, transport, transfer, harbor or receive forced labor or Uyghurs, Kazakhs, Kyrgyz, or members of other persecuted groups out of Xinjiang |
| 2(d)(2)(B)(v) | 149 | A list of facilities and entities, including the Xinjiang Production and Construction Corps, that source material from Xinjiang or from persons working with the government of Xinjiang or the Xinjiang Production and Construction Corps for purposes of the ‘‘poverty alleviation’’ program or the ‘‘pairing-assistance’’ program or any other government-labor scheme that uses forced labor |
1. Rows are not entities
The three tables hold 205 rows. Only 193 distinct names appear in them: 12 rows repeat, character for character, a name already listed in another category. Grouping the names that differ only in punctuation, the spelling of "Co." and "Ltd.", an alias list or an inline acronym leaves 187, the total DHS gave in its announcement of 2026-07-31. These rows name an entity already listed, written differently:
| category | name, as printed |
|---|---|
| (i) | Hetian Haolin Hair Accessories Co. Ltd. (and two aliases: Hotan Haolin Hair Accessories; and Hollin Hair Accessories) |
| (v) | ↳ Hetian Haolin Hair Accessories Co. Ltd. |
| (i) | Hetian Taida Apparel Co., Ltd (and one alias: Hetian TEDA Garment) |
| (v) | ↳ Hetian Taida Apparel Co., Ltd. |
| (i) | Hoshine Silicon Industry (Shanshan) Co., Ltd (including one alias: Hesheng Silicon Industry (Shanshan) Co.) and subsidiaries |
| (v) | ↳ Hoshine Silicon Industry (Shanshan) Co., Ltd., and Subsidiaries |
| (i) | Xinjiang Daqo New Energy, Co. Ltd (including three aliases: Xinjiang Great New Energy Co., Ltd.; Xinjiang Daxin Energy Co., Ltd.; and Xinjiang Daqin Energy Co., Ltd.) |
| (v) | ↳ Xinjiang Daqo New Energy Co., Ltd. (also known as Xinjiang Great New Energy Co., Ltd.; Xinjiang Daxin Energy Co., Ltd.; and Xinjiang Daqin Energy Co., Ltd.) |
| (i) | Xinjiang Production and Construction Corps (including three aliases: XPCC; Xinjiang Corps; and Bingtuan) and its subordinate and affiliated entities |
| (ii) | ↳ Xinjiang Production and Construction Corps (XPCC) and its subordinate and affiliated entities |
| (v) | ↳ Xinjiang Production and Construction Corps (XPCC) and its subordinate and affiliated entities |
| (ii) | Hefei Bitland Information Technology Co., Ltd. (including three aliases: Anhui Hefei Baolongda Information Technology; Hefei Baolongda Information Technology Co., Ltd.; and Hefei Bitland Optoelectronic Technology Co., Ltd.) |
| (v) | ↳ Hefei Bitland Information Technology Co. Ltd. |
So 170 entities appear in one category, 16 in two and 1 in three. A screen that counts matches will count one entity more than once, and a list deduplicated by exact name still holds 6 duplicates.
2. The aliases can be on only one copy
5 of those entities carry their alias list in one category and none in another. For example:
- (i) Hetian Haolin Hair Accessories Co. Ltd. (and two aliases: Hotan Haolin Hair Accessories; and Hollin Hair Accessories) (v) Hetian Haolin Hair Accessories Co. Ltd.
- (i) Hetian Taida Apparel Co., Ltd (and one alias: Hetian TEDA Garment) (v) Hetian Taida Apparel Co., Ltd.
Deduplicate by keeping the first or the last copy, and an alias can disappear from your list. Screen every row, or merge the aliases of every copy before dropping any.
3. The effective date belongs to the row, not the entity
The same entity can carry a different date in each category:
- Xinjiang Daqo New Energy, Co. Ltd: 2022-06-21 in (i), 2024-11-25 in (v)
- Xinjiang Habahe Ashele Copper Co., Ltd.: 2024-08-09 in (ii), 2025-01-15 in (v)
A single "date listed" per entity has to choose one of them. Keep the date with its category.
4. "Last Updated" is older than the newest entries
The page footer read Last Updated 2026-07-31, while 47 rows carried an effective date of 2026-08-03. The footer dates the page's last edit, and a listing can take effect after it. Read each row's own date for when that listing takes effect; neither date says when DHS will next change the list.
5. Aliases are written 6 ways, inside the name
| phrasing | rows |
|---|---|
(also known as …) | 56 |
(formerly known as …) | 11 |
(including N aliases: …) | 6 |
(and N aliases: …) | 1 |
(and one alias: …) | 1 |
(including one alias: …) | 1 |
Beside them, 4 rows carry an inline acronym such as (XPCC) that is not introduced as an alias,
and 8 carry other parentheses that are part of the name, such as a place name in brackets. An alias list can
itself contain parentheses, so a parser that stops at the first closing bracket cuts an alias in half.
The whole thing, runnable
Node 18 or later, no dependencies. It reads DHS's robots.txt, fetches the page once, and prints every measured figure
on this page. Add --csv to also write one CSV row per table row, with its section, date, name, aliases and the
grouping key above: a machine-readable copy dated by your own run.
// Reads DHS's UFLPA Entity List page once and prints what a program matching against it has to know.
// Node 18+, no dependencies. One GET to www.dhs.gov (plus robots.txt). Pass a saved copy instead with: node uflpa-list.mjs page.html
// Add --csv to also write uflpa-entity-list.csv: one row per table row, with its section, date, name, aliases and a group key.
import { readFileSync, writeFileSync } from 'node:fs';
const args = process.argv.slice(2);
const wantCsv = args.includes('--csv');
const file = args.find((a) => a !== '--csv');
const URL_ = 'https://www.dhs.gov/uflpa-entity-list';
const UA = 'uflpa-list-reader/1.0 (reads the public page once)';
const decode = (s) =>
s.replace(/ /g, ' ').replace(/&/g, '&').replace(/"/g, '"').replace(/'|'/g, "'")
.replace(/</g, '<').replace(/>/g, '>').replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)));
const text = (html) => decode(html.replace(/<[^>]+>/g, ' ')).replace(/\s+/g, ' ').trim();
let html;
if (file) {
html = readFileSync(file, 'utf8');
console.log(`source file ${file}`);
} else {
const robots = await fetch('https://www.dhs.gov/robots.txt', { headers: { 'user-agent': UA } });
const rules = await robots.text();
const blocked = /^Disallow:\s*\/uflpa-entity-list\b/im.test(rules);
console.log(`${new Date().toISOString()} GET /robots.txt ${robots.status}; page disallowed: ${blocked}`);
if (blocked) process.exit(2);
await new Promise((r) => setTimeout(r, 5000));
const res = await fetch(URL_, { headers: { 'user-agent': UA } });
html = await res.text();
console.log(`${new Date().toISOString()} GET ${URL_} ${res.status}, ${html.length} characters, content-type ${JSON.stringify(res.headers.get('content-type'))}`);
if (!res.ok) process.exit(1);
}
const lastUpdated = /Last Updated:\s*(\d{2})\/(\d{2})\/(\d{4})/.exec(text(html));
console.log(`page Last Updated ${lastUpdated ? `${lastUpdated[3]}-${lastUpdated[1]}-${lastUpdated[2]}` : 'NOT FOUND'}`);
console.log(`machine-readable links on the page: ${(html.match(/href="[^"]+\.(csv|json|xlsx?)"/gi) ?? []).length}`);
// One table per statutory category, each under "<strong>heading</strong><br><a>Section 2(d)(2)(B)(…)</a>".
const MONTHS = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'];
const iso = (d) => {
const m = /^([A-Za-z]+) (\d{1,2}), (\d{4})$/.exec(d);
const mo = m ? MONTHS.indexOf(m[1].toLowerCase()) : -1;
return mo < 0 ? null : `${m[3]}-${String(mo + 1).padStart(2, '0')}-${m[2].padStart(2, '0')}`;
};
const rows = [];
let prev = 0;
for (const t of html.matchAll(/<table\b[^>]*>[\s\S]*?<\/table>/gi)) {
const headings = [...html.slice(prev, t.index).matchAll(/<strong\b[^>]*>((?:(?!<\/?strong\b)[\s\S])*?)<\/strong>\s*(?:<br\s*\/?>\s*)?<a\b[^>]*>\s*(Section\s+2\(d\)\(2\)\(B\)\([ivx]+\))\s*<\/a>/gi)];
const h = headings.at(-1);
const section = h ? text(h[2]).replace('Section ', '') : 'none';
let n = 0;
for (const tr of t[0].matchAll(/<tr\b[^>]*>([\s\S]*?)<\/tr>/gi)) {
if (/<th\b/i.test(tr[1])) continue;
const cells = [...tr[1].matchAll(/<td\b[^>]*>([\s\S]*?)<\/td>/gi)].map((c) => text(c[1]));
if (cells.length !== 2 || !cells[0]) continue;
rows.push({ section, name: cells[0], date: iso(cells[1]) ?? cells[1] });
n++;
}
console.log(`table ${section}: ${n} rows under ${JSON.stringify(h ? text(h[1]) : null)}`);
prev = t.index + t[0].length;
}
// Grouping: the same entity written differently. Drop an alias parenthetical (nested parentheses allowed), an inline
// all-capitals acronym such as "(XPCC)", punctuation, and the words Co and Ltd; compare lower-cased.
const ALIAS = /\s*\((?:and|including)\s+\w+\s+alias(?:es)?:|\s*\((?:also|formerly) known as\b/i;
function primary(name) {
const m = ALIAS.exec(name);
if (!m) return name;
let depth = 0;
for (let i = m.index + m[0].indexOf('('); i < name.length; i++) {
if (name[i] === '(') depth++;
if (name[i] === ')' && --depth === 0) return (name.slice(0, m.index) + name.slice(i + 1)).trim();
}
return name;
}
const key = (name) =>
primary(name).replace(/\s*\([A-Z]{2,}\)/g, '').toLowerCase().replace(/[().,'"&-]/g, ' ')
.replace(/\b(co|ltd)\b/g, ' ').replace(/\s+/g, ' ').trim();
const cells = new Map();
for (const r of rows) cells.set(r.name.toLowerCase(), [...(cells.get(r.name.toLowerCase()) ?? []), r]);
const groups = new Map();
for (const r of rows) groups.set(key(r.name), [...(groups.get(key(r.name)) ?? []), r]);
console.log(`rows ${rows.length}; distinct name cells ${cells.size}; rows repeating a cell exactly ${rows.length - cells.size}; entities after grouping ${groups.size}`);
for (const g of groups.values()) {
const spellings = [...new Set(g.map((r) => r.name))];
if (spellings.length > 1) console.log(`written differently: ${g.map((r) => `[${r.section}] ${JSON.stringify(r.name)}`).join(' | ')}`);
}
for (const g of groups.values()) {
const dates = [...new Set(g.map((r) => r.date))];
if (dates.length > 1) console.log(`different dates: ${JSON.stringify(primary(g[0].name))} ${g.map((r) => `${r.section} ${r.date}`).join(' | ')}`);
}
const listedIn = [...groups.values()].map((g) => new Set(g.map((r) => r.section)).size);
console.log(`entities in one category ${listedIn.filter((n) => n === 1).length}; two ${listedIn.filter((n) => n === 2).length}; three ${listedIn.filter((n) => n === 3).length}`);
const latest = rows.map((r) => r.date).filter((d) => /^\d{4}-/.test(d)).sort().at(-1);
const lu = lastUpdated ? `${lastUpdated[3]}-${lastUpdated[1]}-${lastUpdated[2]}` : null;
console.log(`latest effective date ${latest}; rows effective after Last Updated ${lu ? rows.filter((r) => r.date > lu).length : 'n/a'}`);
const phrasings = {};
for (const r of rows) for (const m of r.name.matchAll(/\((and \w+ alias(?:es)?:|including \w+ alias(?:es)?:|also known as|formerly known as)/gi)) {
const k = m[1].toLowerCase().replace(/^(and|including) \w+ alias(es)?:/, '$1 N alias$2:');
phrasings[k] = (phrasings[k] ?? 0) + 1;
}
console.log(`alias phrasings ${JSON.stringify(phrasings)}`);
console.log(`inline acronyms ${rows.filter((r) => /\([A-Z]{2,}\)/.test(r.name)).length}; names with parentheses that are not aliases ${rows.filter((r) => /\((?!and |including |also known|formerly known|[A-Z]{2,}\))/.test(r.name)).length}`);
if (wantCsv) {
// The alias list is the text inside the alias parenthetical, split on "; " and "; and ".
const aliasesOf = (name) => {
const m = ALIAS.exec(name);
if (!m) return '';
let depth = 1, i = m.index + m[0].length;
for (; i < name.length && depth > 0; i++) { if (name[i] === '(') depth++; if (name[i] === ')') depth--; }
return name.slice(m.index + m[0].length, i - 1).split(/;\s*(?:and\s+)?/).map((a) => a.trim()).filter(Boolean).join(' | ');
};
const q = (v) => `"${String(v).replace(/"/g, '""')}"`;
const lines = ['section,effective_date,name_as_printed,primary_name,aliases,group_key'];
for (const r of rows) lines.push([r.section, r.date, r.name, primary(r.name), aliasesOf(r.name), key(r.name)].map(q).join(','));
writeFileSync('uflpa-entity-list.csv', lines.join('\n') + '\n');
console.log(`wrote uflpa-entity-list.csv (${rows.length} rows)`);
}
console.log(`exit 0`);
Its output on :
$ node --version
v24.20.0
$ node uflpa-list.mjs
2026-09-15T10:49:20.129Z GET /robots.txt 200; page disallowed: false
2026-09-15T10:49:25.270Z GET https://www.dhs.gov/uflpa-entity-list 200, 137442 characters, content-type "text/html; charset=utf-8"
page Last Updated 2026-07-31
machine-readable links on the page: 0
table 2(d)(2)(B)(i): 9 rows under "A list of entities in Xinjiang that mine, produce, or manufacture wholly or in part any goods, wares, articles and merchandise with forced labor"
table 2(d)(2)(B)(ii): 47 rows under "A list of entities working with the government of Xinjiang to recruit, transport, transfer, harbor or receive forced labor or Uyghurs, Kazakhs, Kyrgyz, or members of other persecuted groups out of Xinjiang"
table 2(d)(2)(B)(v): 149 rows under "A list of facilities and entities, including the Xinjiang Production and Construction Corps, that source material from Xinjiang or from persons working with the government of Xinjiang or the Xinjiang Production and Construction Corps for purposes of the ‘‘poverty alleviation’’ program or the ‘‘pairing-assistance’’ program or any other government-labor scheme that uses forced labor"
rows 205; distinct name cells 193; rows repeating a cell exactly 12; entities after grouping 187
written differently: [2(d)(2)(B)(i)] "Hetian Haolin Hair Accessories Co. Ltd. (and two aliases: Hotan Haolin Hair Accessories; and Hollin Hair Accessories)" | [2(d)(2)(B)(v)] "Hetian Haolin Hair Accessories Co. Ltd."
written differently: [2(d)(2)(B)(i)] "Hetian Taida Apparel Co., Ltd (and one alias: Hetian TEDA Garment)" | [2(d)(2)(B)(v)] "Hetian Taida Apparel Co., Ltd."
written differently: [2(d)(2)(B)(i)] "Hoshine Silicon Industry (Shanshan) Co., Ltd (including one alias: Hesheng Silicon Industry (Shanshan) Co.) and subsidiaries" | [2(d)(2)(B)(v)] "Hoshine Silicon Industry (Shanshan) Co., Ltd., and Subsidiaries"
written differently: [2(d)(2)(B)(i)] "Xinjiang Daqo New Energy, Co. Ltd (including three aliases: Xinjiang Great New Energy Co., Ltd.; Xinjiang Daxin Energy Co., Ltd.; and Xinjiang Daqin Energy Co., Ltd.)" | [2(d)(2)(B)(v)] "Xinjiang Daqo New Energy Co., Ltd. (also known as Xinjiang Great New Energy Co., Ltd.; Xinjiang Daxin Energy Co., Ltd.; and Xinjiang Daqin Energy Co., Ltd.)"
written differently: [2(d)(2)(B)(i)] "Xinjiang Production and Construction Corps (including three aliases: XPCC; Xinjiang Corps; and Bingtuan) and its subordinate and affiliated entities" | [2(d)(2)(B)(ii)] "Xinjiang Production and Construction Corps (XPCC) and its subordinate and affiliated entities" | [2(d)(2)(B)(v)] "Xinjiang Production and Construction Corps (XPCC) and its subordinate and affiliated entities"
written differently: [2(d)(2)(B)(ii)] "Hefei Bitland Information Technology Co., Ltd. (including three aliases: Anhui Hefei Baolongda Information Technology; Hefei Baolongda Information Technology Co., Ltd.; and Hefei Bitland Optoelectronic Technology Co., Ltd.)" | [2(d)(2)(B)(v)] "Hefei Bitland Information Technology Co. Ltd."
different dates: "Xinjiang Daqo New Energy, Co. Ltd" 2(d)(2)(B)(i) 2022-06-21 | 2(d)(2)(B)(v) 2024-11-25
different dates: "Xinjiang Habahe Ashele Copper Co., Ltd." 2(d)(2)(B)(ii) 2024-08-09 | 2(d)(2)(B)(v) 2025-01-15
entities in one category 170; two 16; three 1
latest effective date 2026-08-03; rows effective after Last Updated 47
alias phrasings {"and N aliases:":1,"and N alias:":1,"including N alias:":1,"including N aliases:":6,"formerly known as":11,"also known as":56}
inline acronyms 4; names with parentheses that are not aliases 8
exit 0
exit 0
What this does not cover
Whether a supplier is related to a listed entity, what "and subsidiaries" reaches, and what a match means for a shipment. The requirements guide covers what DHS and CBP say about those.
Two ways a name defeats a match against this list
A name written in Chinese characters cannot be compared at all. Every row on DHS's page is Latin text, so a matcher working on a normalized Latin form has nothing to compare a CJK string to — it reduces to an empty string, which scores zero against every row. The failure mode that matters is what happens next: an empty comparison reported as "no match" is a clean screen for a supplier that was never screened. Our own API returns an error for that input rather than a status, in these words:
"The supplied name contains no Latin letters or digits after normalization, so this source could not be queried with it at all… This is a coverage gap, not a clean result: supply a romanized or transliterated form of the name to screen it."
And a romanized name typed without its spaces will miss too. A compacted string is one token that appears in neither name's token set, so what is left is an edit-distance comparison a short string cannot win. We measured it on 97 real listed entities: a company's short name found its record 98% of the time spelled with spaces and 4% without, and half the misses scored below the floor where a near miss would be reported at all — the measurement and the data.
Both are checkable before you screen anything. The free screening name checker flags the names in a list you paste that a screen cannot compare well — the compacted ones, the ones that normalize to nothing, the ones of only short words. It runs in your browser and nothing you paste is sent anywhere.
Aervik Labs' US Import Risk Screening API reads this page for its UFLPA check:
it screens every row and every alias, and returns each match's category as basis.
Working with the shipment response? Use the runnable response-handling example to distinguish source errors, incomplete coverage, potential matches, and a completed no-match.