Vendor screening data · implementation
SAM.gov's Exclusions extract as data: ten traps in screening vendors against it
SAM.gov publishes its federal exclusions as a daily file that any SAM.gov account holder with an API key can download. Screening vendor names against it from code means turning a ZIP of CSV into a list, and several things about that file change the answer. Each trap below was measured on the file dated , downloaded from the live API on , with the script and its output at the end of the page.
Where the data lives
# the daily file of active exclusions: one request, a redirect, then a ZIP GET https://api.sam.gov/data-services/v1/extracts?api_key=YOUR_KEY&fileType=EXCLUSION&date=MM/DD/YYYY
GSA documents the download on its extracts API page, which says of the daily exclusion files: "They are produced every day. Kindly check after 7 AM Eastern time." and "These files contain all the active exclusions." The columns are described in GSA's Exclusions extract layout, Version 1.5, dated April 16, 2026. Both were read on 2026-09-15; quotes below marked "the API page" and "the layout" come from them. A personal API key is on your SAM.gov account details page.
1. The download is a redirect to an empty body
The API page's response-code table lists 200 ("Successful. Data will be returned in JSON format."), 400, 406 and 415.
The request above returned HTTP 302 with a 0-byte body and no content-type.
Its Location pointed at falextracts.s3.amazonaws.com, and following it returned HTTP 200,
application/zip, 11,843,027 bytes. A client that does not follow redirects keeps the empty body: curl without -L writes
it to your output file and exits 0. The location did not carry the API key; our second request sent none and got the file.
In this run the first response arrived 226 seconds after the request was sent, so a short client timeout can
give up before the redirect arrives.
The ZIP held one file of about 78 MB:
SAM_Exclusions_Public_Extract_V2_26260.CSV
Its last five digits are the date the file was generated: 26260 is 2026, day 260, so 2026-09-17. Take
the date from the name, not from when you downloaded it. The layout's own example does not follow that rule: it says
SAM_ "is the daily file for Aug 30, 2024", and day 226 of 2024 is
August 13, 2024.
2. Ten requests a day, and no header counts them down
The API page's rate-limit table gives a non-federal user with no role in SAM.gov a personal API key limited to "10 requests/day"; a non-federal user with a role, a federal user or a non-federal system account gets "1,000 requests/day". The response to the extract request carried no rate-limit headers, so nothing in a successful response says how many requests are left. Our own non-federal key reached that limit on 2026-08-27 and got HTTP 429. A screen that spends one request per vendor name on such a key has ten names a day. The daily file took one request to api.sam.gov in this run, and it holds every row at once.
3. The layout does not describe this file
The layout is GSA's field-by-field description of this file. Where it and file 26260 disagree:
| column | the layout says | file 26260 has |
|---|---|---|
| Active Date | “Format: MM/DD/YYYY” | YYYY-MM-DD on 157,449 rows; blank on 11,016 |
| Termination Date | “Format: MM/DD/YYYY or Indefinite” | YYYY-MM-DD on 9,350 rows; Indefinite on 159,115 |
| Creation Date | column 31, “Format: MM/DD/YYYY” | headed Creation_Date; YYYY-MM-DD on all 168,465 rows |
| Record Status | “this column is always blank” | Active on 168,465 of 168,465 rows |
| Exclusion Program | “Reciprocal, Nonreciprocal or Procurement”; for records created on or after August 25, 1995, “the value will always be Reciprocal” | Reciprocal 159,320, NonProcurement 9,113, Procurement 32; all 9,145 others created on or after that date |
| CT Code | “Exclusions created after August 2012 will not have CT Codes.” | filled on 12,078 rows created after 2012-08-31 |
| Cross-Reference | maximum length 200 | 4,639 values longer, the longest 3,044 characters |
| Open Data Flag | “indicates whether the Business Name and Physical Address sourced from EVS or Prior provider” | filled on 0 rows |
| file name | “SAM_Exclusions_Public_V2_Extract_YYDDD.ZIP” | a CSV inside named SAM_Exclusions_Public_Extract_V2_26260.CSV |
The API page names the daily file SAM_, which matches the name of the CSV inside apart from the extension.
A parser built from the layout's date formats rejects every date in the file, and one that looks up a column named
"Creation Date" finds nothing. Read columns by the header the file carries, check every value against the shape you
expect, and stop on a header or a shape you have not seen before rather than guessing.
4. A row is not a line
The file holds 168,466 CSV records, header included, and 168,691 line-feed characters: 1,281 rows
carry a line break inside Additional Comments. It also holds 2,084 carriage returns, only 7 of them
followed by a line feed. wc -l counts the line feeds, and a reader that also splits on carriage returns
breaks more rows apart. Parse with a CSV reader that honors quotes (in Python, open the file with
newline='', as the csv module's documentation says) and check that every row has 31 fields: all
168,465 did.
5. Name is blank on 79% of rows, and most of the rest are not Firms
| Classification | rows | Name filled |
|---|---|---|
| Individual | 133,274 | 0 |
| Special Entity Designation | 25,585 | 25,585 |
| Firm | 8,284 | 8,284 |
| Vessel | 1,322 | 1,322 |
The layout says Name is "not applicable for Individual exclusions", and it was blank on all 133,274 of them;
those rows split a person's name across Prefix, First, Middle, Last and Suffix. Special Entity Designation rows
outnumber Firm rows about 3 to 1, and 23,723 of
them come from OFAC (trap 6). A screen that keeps only Classification = Firm compares a vendor with
8,284 of the 35,191 rows that carry a Name. The layout lists the four values and does not define
Special Entity Designation.
6. 41,712 rows are OFAC's
Excluding Agency is OFAC on 41,712 rows: Special Entity Designation 23,723, Individual 16,666, Vessel 1,322, Firm 1.
Every one has Exclusion Program Reciprocal and Exclusion Type Prohibition/Restriction, and
no Exclusion Type value in the file names OFAC,
so Excluding Agency is the column that separates them. 9,609 of them carry an OFAC program tag in CT Code,
most often 03-SDN-01 (3,562), 03-SDGT-01 (3,350), 03-SDNTK-01 (1,846).
Country is XUN on 8,225 rows, 8,222 of them OFAC's. XUN falls in the range of codes
ISO 3166-1 leaves for user assignment, so it names no country, and a screen that compares a vendor only with rows from
its own country never reaches those rows. Country is blank on another 634, and Excluding Agency is
blank on 2,424. A count of "exclusions found" mixes OFAC's rows with other agencies' exclusions unless you
split it on Excluding Agency.
7. An Exclusion Type changed spelling in 2026
| Exclusion Type | rows | Creation_Date range |
|---|---|---|
| Prohibition/Restriction | 135,313 | 1997-02-03 to 2026-09-09 |
| Ineligible (Proceedings Completed) | 29,061 | 1997-02-03 to 2026-02-27 |
| Ineligible (Proceedings Pending) | 2,691 | 1997-02-03 to 2026-09-16 |
| Ineligible (Proceedings Complete) | 1,190 | 2026-03-03 to 2026-09-16 |
| Voluntary Exclusion | 210 | 1997-02-03 to 2026-09-01 |
"Ineligible (Proceedings Completed)" is on rows created up to 2026-02-27; "Ineligible (Proceedings Complete)", one letter shorter, is on rows created from 2026-03-03. A filter written for the first string misses all 1,190 rows of that type created since 2026-03-03. Exclusion Program has the same problem against the layout (trap 3). Map every value you meet to your own code, and stop on one you have not mapped.
8. 4,005 rows repeat another row under a new SAM Number
SAM Number was different on all 168,465 rows, so a check for identical rows finds none. Leave SAM Number out and 4,005 rows repeat every other column of another row exactly, in 3,864 groups of up to 6 copies: Special Entity Designation 2,132, Individual 1,835, Firm 28, Vessel 10. By agency, the most are OFAC (3,468), DOJ (279), OPM (231). A vendor that matches one of those records matches every copy, so count and report matches per distinct record.
Unique Entity ID is not a record key either: 7,346 IDs appear on more than one row, covering 16,600 rows, and for 5,089 of those IDs the rows differ in more than SAM Number. Deduplicating on it merges records that are not copies of each other.
9. Identifiers are partial, and other names share one column
The layout says only Firm and Special Entity Designation records "are required to have a Unique Entity ID". In the file it was filled on 7,687 of 8,284 Firm rows, 25,581 of 25,585 Special Entity Designation rows and 911 of 1,322 Vessel rows; 1 value was 5 characters long where the layout says 12. CAGE was filled on 424 rows, all of them Firm. A screen that matches on UEI alone never sees the 597 Firm rows without one; match the name as well.
The layout's column for other names is Cross-Reference, "other names/aliases with which the entity being excluded has been identified": one free-text value per row, starting "(also", filled on 18,505 Special Entity Designation, 3,027 Firm and 193 Vessel rows. The names inside it are separated by commas, and a comma is part of the Name itself on 3,688 Firm and 2,428 Special Entity Designation rows, so splitting on commas does not recover the list reliably. On organization rows the column can also hold names of people. A screen that reads only Name never finds a vendor listed under one of its other names, and one that stores Cross-Reference in a column sized from the layout's 200 characters truncates 4,639 values.
10. The Dun & Bradstreet terms name a created date, and only Creation_Date holds one
SAM.gov's Terms of Use, read 2026-09-15, in "Limitation on Permissible Use of Dun & Bradstreet, Inc. Data", define "D&B Open Data" as "the following data elements: Legal Business Name, Street Address, City Name, State/Province Name, Country Name, County Code, State/Province Code, State/Province Abbreviation, ZIP/Postal Code, Country Name and Country Code." The records they apply to include "all exclusions records with a created date earlier than 4/4/2022". Users agree not to use D&B Open Data "without giving written attribution to the source of such data (i.e., D&B)" and not to "access, use or disseminate D&B Open Data in bulk (i.e., in amounts sufficient for use as an original source or as a substitute for the product and/or service being licensed hereunder)".
The Terms add: "These records show D&B as the Entity Validation Service (EVS) Source in records with D&B data." The layout's Open Data Flag "indicates whether the Business Name and Physical Address sourced from EVS or Prior provider"; it was filled on 0 of 168,465 rows, so it shows that source on none. The file's only created date is Creation_Date, "the date the exclusion was created in SAM" in the layout's words. Active Date answers a different question: it was blank on 11,016 rows and differed from Creation_Date on 145,279 of the 157,449 that had one, with Creation_Date the later date on 143,453. Rows with a Creation_Date before 2022-04-04:
| Classification | created before 2022-04-04 | rows |
|---|---|---|
| Firm | 6,696 | 8,284 |
| Special Entity Designation | 9,298 | 25,585 |
| Vessel | 411 | 1,322 |
| Individual | 113,450 | 133,274 |
In this file, the columns closest to the data elements the Terms list are Name and the address columns (Address 1 to 4, City, State / Province, Country and Zip Code). Blank (Deprecated), which the layout says "held the prior 9-digit identifier for exclusions with a classification of Firm", was filled on 0 rows. Nor does Termination Date tell you what is current: 13 rows carried a Termination Date before the file's own date and still said Active.
Testing a parser against these traps
Each condition below occurs in file 26260. Write test rows for them with invented organization names rather than copying rows: the file lists individuals, and its older organization rows fall under trap 10.
- comments holding line feeds and bare carriage returns inside quotes (trap 4)
- a blank Active Date and an Indefinite Termination Date (trap 3)
- both "Ineligible (Proceedings Completed)" and "Ineligible (Proceedings Complete)" (trap 7)
- an OFAC row with Country
XUN(trap 6) - two rows identical except for SAM Number, and one UEI on rows that differ in more than SAM Number (trap 8)
- a Firm with no UEI, a Name containing a comma, and a Cross-Reference longer than 200 characters (trap 9)
- rows created on each side of 2022-04-04, and an Active Date that differs from Creation_Date (trap 10)
Add two that do not occur in it yet, and make both stop the parse: the header with Creation_Date renamed (trap 3), and an Exclusion Type you have not mapped (trap 7).
The whole thing, runnable
Python 3.9 or later, standard library only. It makes one request to api.sam.gov, follows the redirect itself, saves the ZIP, and prints every figure on this page; pass a saved ZIP instead to re-read it with no request. It prints counts, column names and coded values, and never a name, an address, a Cross-Reference or your key.
# sam_exclusions.py -- measures the traps in SAM.gov's daily Exclusions extract.
# Python 3.9+, standard library only. Needs a SAM.gov API key: the Public API Key field at sam.gov/workspace/profile/account-details
# SAM_GOV_API_KEY=... python3 sam_exclusions.py one API request for today's file; saves the ZIP
# python3 sam_exclusions.py saved.zip re-reads a saved ZIP and makes no request
# Prints counts, column names and coded values only. It never prints a name, an address, a Cross-Reference
# or your key: the extract lists individuals as well as companies.
import collections, csv, datetime, hashlib, io, json, os, re, sys, time, zipfile
import urllib.error, urllib.parse, urllib.request
from zoneinfo import ZoneInfo
API = 'https://api.sam.gov/data-services/v1/extracts'
UA = 'sam-exclusions-extract-traps/1.0 (one download a day)'
now = lambda: datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
js = lambda v: json.dumps(v, ensure_ascii=False)
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, *args, **kwargs):
return None # T1: surface the redirect instead of following it silently
if len(sys.argv) > 1:
zip_bytes = open(sys.argv[1], 'rb').read()
print(f'source file {os.path.basename(sys.argv[1])}, {len(zip_bytes)} bytes')
else:
key = os.environ.get('SAM_GOV_API_KEY') or sys.exit('Set SAM_GOV_API_KEY')
# GSA: the daily exclusion files "are produced every day. Kindly check after 7 AM Eastern time."
query = 'fileType=EXCLUSION&date=' + datetime.datetime.now(ZoneInfo('America/New_York')).strftime('%m/%d/%Y')
print(f'run at {now()}')
request = urllib.request.Request(f'{API}?api_key={urllib.parse.quote(key)}&{query}', headers={'User-Agent': UA})
started = time.monotonic()
try:
res = urllib.request.build_opener(NoRedirect).open(request, timeout=600)
status, headers, body = res.status, res.headers, res.read()
except urllib.error.HTTPError as e:
status, headers, body = e.code, e.headers, e.read()
print(f'{now()} GET /data-services/v1/extracts?{query} -> {status}, content-type {js(headers.get("content-type"))}, body {len(body)} bytes')
print(f'T1 first response after {round(time.monotonic() - started)} seconds')
# T2: whatever rate-limit headers the gateway sends, names and numbers only.
print('T2 rate-limit headers ' + js({k.lower(): v for k, v in headers.items() if re.search(r'rate|limit|retry|quota', k, re.I)}))
location = headers.get('location')
if 300 <= status < 400 and location:
target = urllib.parse.urljoin(API, location)
print(f'T1 redirect to host {urllib.parse.urlsplit(target).hostname}; the location carries your key: {key in location or urllib.parse.quote(key) in location}')
res = urllib.request.urlopen(urllib.request.Request(target, headers={'User-Agent': UA}), timeout=600)
zip_bytes = res.read()
print(f'{now()} GET <location> -> {res.status}, content-type {js(res.headers.get("content-type"))}, content-disposition {js(res.headers.get("content-disposition"))}, {len(zip_bytes)} bytes')
elif status == 200 and body[:2] == b'PK':
print('T1 no redirect: the file came back in the first response')
zip_bytes = body
else:
print(f'T1 no file; body {js(body.decode("utf-8", "replace").replace(key, "<key>")[:300])}')
sys.exit(2)
zf = zipfile.ZipFile(io.BytesIO(zip_bytes))
print('T1 zip entries ' + js([f'{i.filename} {i.compress_size} -> {i.file_size} bytes' for i in zf.infolist()]))
name = next(i.filename for i in zf.infolist() if i.filename.lower().endswith('.csv'))
if len(sys.argv) == 1:
open(name[:-4] + '.ZIP', 'wb').write(zip_bytes)
print(f'saved {name[:-4]}.ZIP')
m = re.search(r'_(\d{2})(\d{3})\.csv$', name, re.I) or sys.exit(f'no YYDDD date in the file name {name}')
file_date = (datetime.date(2000 + int(m[1]), 1, 1) + datetime.timedelta(days=int(m[2]) - 1)).isoformat()
print(f'T1 the name says {m[1]}{m[2]}: 20{m[1]}, day {int(m[2])} = {file_date}')
raw = zf.read(name) # T4: count line-break bytes before any CSV parsing
lf, cr, crlf = raw.count(b'\n'), raw.count(b'\r'), raw.count(b'\r\n')
del raw
reader = csv.reader(io.TextIOWrapper(zf.open(name), encoding='utf-8', newline=''))
header = next(reader)
print(f'T3 header {len(header)} columns ' + js(header))
col = {h: i for i, h in enumerate(header)} # a renamed column raises KeyError below: fail loudly, never guess
Counter = collections.Counter
widths, klass, named, broken, broken_cols = Counter(), Counter(), Counter(), 0, Counter()
shapes = {h: Counter() for h in ('Active Date', 'Termination Date', 'Creation_Date')}
status_, program, etype, agency_blank = Counter(), Counter(), Counter(), 0
type_dates = collections.defaultdict(lambda: ['9999', '0000'])
not_recip_after_1995 = ct_after_2012 = xref_over_200 = xref_longest = 0
xref_not_also = odf = blank_dep = 0
ofac, ofac_prog, ofac_type, ofac_ct, type_naming_ofac = Counter(), Counter(), Counter(), Counter(), 0
xun, xun_ofac, country_blank, ofac_with_ct = 0, 0, 0, 0
uei, cage, xref, uei_len, uei_rows, comma = Counter(), Counter(), Counter(), Counter(), Counter(), Counter()
uei_contents = collections.defaultdict(set)
digests, sam_numbers = Counter(), set()
dup_class, dup_agency = Counter(), Counter()
both = differ = created_later = active_blank = indefinite = dated = ended_before_file = 0
pre_2022 = Counter()
for row in reader:
widths[len(row)] += 1
g = lambda h: row[col[h]]
c = g('Classification')
klass[c] += 1
named[c] += g('Name') != ''
hit = [h for h, v in zip(header, row) if '\n' in v or '\r' in v]
broken += bool(hit)
broken_cols.update(hit)
for h in shapes: # T3: the layout says MM/DD/YYYY; record the shape of every value
shapes[h][re.sub(r'\d', '9', g(h))] += 1
created, active, ends = g('Creation_Date'), g('Active Date'), g('Termination Date')
status_[g('Record Status')] += 1
program[g('Exclusion Program')] += 1
not_recip_after_1995 += g('Exclusion Program') != 'Reciprocal' and created >= '1995-08-25'
t = g('Exclusion Type')
etype[t] += 1
type_naming_ofac += 'OFAC' in t
type_dates[t] = [min(type_dates[t][0], created), max(type_dates[t][1], created)]
ct_after_2012 += g('CT Code') != '' and created > '2012-08-31'
x = g('Cross-Reference')
xref_over_200 += len(x) > 200
xref_longest = max(xref_longest, len(x))
xref_not_also += x != '' and not x.startswith('(also')
odf += g('Open Data Flag') != ''
blank_dep += g('Blank (Deprecated)') != ''
agency_blank += g('Excluding Agency') == ''
if g('Excluding Agency') == 'OFAC':
ofac[c] += 1
ofac_prog[g('Exclusion Program')] += 1
ofac_type[t] += 1
ofac_ct[g('CT Code') or '(blank)'] += 1
ofac_with_ct += g('CT Code') != ''
xun += g('Country') == 'XUN'
xun_ofac += g('Country') == 'XUN' and g('Excluding Agency') == 'OFAC'
country_blank += g('Country') == ''
uei[c] += g('Unique Entity ID') != ''
cage[c] += g('CAGE') != ''
xref[c] += x != ''
comma[c] += ',' in g('Name')
sam_numbers.add(g('SAM Number'))
# T8: a digest of every column except SAM Number, which is unique per row
d = hashlib.sha1('\x1f'.join(v for h, v in zip(header, row) if h != 'SAM Number').encode()).digest()
if g('Unique Entity ID'):
uei_len[len(g('Unique Entity ID'))] += 1
uei_rows[g('Unique Entity ID')] += 1
uei_contents[g('Unique Entity ID')].add(d)
digests[d] += 1
if digests[d] > 1:
dup_class[c] += 1
dup_agency[g('Excluding Agency') or '(blank)'] += 1
active_blank += active == ''
if active:
both += 1
differ += active != created
created_later += created > active
indefinite += ends == 'Indefinite'
if re.fullmatch(r'\d{4}-\d{2}-\d{2}', ends):
dated += 1
ended_before_file += ends < file_date
pre_2022[c] += created < '2022-04-04'
rows = sum(widths.values())
top = lambda counter, n=None: dict(counter.most_common(n))
print(f'T3 rows {rows}; fields per row {js(top(widths))}')
print('T3 date shapes ' + js({h: top(s) for h, s in shapes.items()}))
print(f'T3 Record Status {js(top(status_))}')
print(f'T3 Exclusion Program {js(top(program))}; not Reciprocal and created on or after 1995-08-25: {not_recip_after_1995}')
print(f'T3 CT Code filled on rows created after 2012-08-31: {ct_after_2012}')
print(f'T3 Cross-Reference longer than 200 characters: {xref_over_200}; longest {xref_longest}; filled values not starting "(also": {xref_not_also}')
print(f'T3 Open Data Flag filled on {odf} rows; Blank (Deprecated) filled on {blank_dep} rows')
print(f'T4 LF characters {lf}; CR characters {cr}; CRLF pairs {crlf}; CSV records including the header {rows + 1}; rows with a line break inside a field {broken} {js(top(broken_cols))}')
print(f'T5 Classification {js(top(klass))}; Name filled {js({k: named[k] for k in klass})}')
print(f'T6 Excluding Agency OFAC {sum(ofac.values())} {js(top(ofac))}; Exclusion Program {js(top(ofac_prog))}; Exclusion Type {js(top(ofac_type))}; Exclusion Type values naming OFAC {type_naming_ofac}')
print(f'T6 OFAC rows with a CT Code {ofac_with_ct}, most common {js(top(ofac_ct, 8))}; Country XUN {xun} (OFAC {xun_ofac}); Country blank {country_blank}; Excluding Agency blank {agency_blank}')
print(f'T7 Exclusion Type {js(top(etype))}')
print('T7 Creation_Date range by Exclusion Type ' + js({t: type_dates[t] for t in etype}))
extra = sum(v - 1 for v in digests.values() if v > 1)
groups = sum(1 for v in digests.values() if v > 1)
print(f'T8 SAM Number distinct {len(sam_numbers)} of {rows}; rows repeating every other column {extra} in {groups} groups, at most {max(digests.values())} copies; by classification {js(top(dup_class))}; by agency {js(top(dup_agency, 4))}')
print(f'T8 Unique Entity IDs on more than one row {sum(1 for v in uei_rows.values() if v > 1)}, covering {sum(v for v in uei_rows.values() if v > 1)} rows; on rows that differ in more than SAM Number {sum(1 for v in uei_contents.values() if len(v) > 1)}')
print(f'T9 Unique Entity ID filled {js({k: uei[k] for k in klass})}; lengths {js(top(uei_len))}; CAGE filled {js({k: cage[k] for k in klass})}; Cross-Reference filled {js({k: xref[k] for k in klass})}; Name containing a comma {js({k: comma[k] for k in klass})}')
print(f'T10 Active Date blank {active_blank}; filled {both}, different from Creation_Date {differ}, Creation_Date later {created_later}')
print(f'T10 Termination Date Indefinite {indefinite}; a date {dated}, before the file date {ended_before_file}')
print(f'T10 Creation_Date before 2022-04-04 {js({k: pre_2022[k] for k in klass})}')
Its output on (counts move whenever SAM.gov publishes a new file):
$ python3 --version
Python 3.9.6
$ SAM_GOV_API_KEY=... python3 sam_exclusions.py
run at 2026-09-17T20:56:21Z
2026-09-17T21:00:06Z GET /data-services/v1/extracts?fileType=EXCLUSION&date=09/17/2026 -> 302, content-type null, body 0 bytes
T1 first response after 226 seconds
T2 rate-limit headers {}
T1 redirect to host falextracts.s3.amazonaws.com; the location carries your key: False
2026-09-17T21:00:07Z GET <location> -> 200, content-type "application/zip", content-disposition null, 11843027 bytes
T1 zip entries ["SAM_Exclusions_Public_Extract_V2_26260.CSV 11842845 -> 78208116 bytes"]
saved SAM_Exclusions_Public_Extract_V2_26260.ZIP
T1 the name says 26260: 2026, day 260 = 2026-09-17
T3 header 31 columns ["Classification", "Name", "Prefix", "First", "Middle", "Last", "Suffix", "Address 1", "Address 2", "Address 3", "Address 4", "City", "State / Province", "Country", "Zip Code", "Open Data Flag", "Blank (Deprecated)", "Unique Entity ID", "Exclusion Program", "Excluding Agency", "CT Code", "Exclusion Type", "Additional Comments", "Active Date", "Termination Date", "Record Status", "Cross-Reference", "SAM Number", "CAGE", "NPI", "Creation_Date"]
T3 rows 168465; fields per row {"31": 168465}
T3 date shapes {"Active Date": {"9999-99-99": 157449, "": 11016}, "Termination Date": {"Indefinite": 159115, "9999-99-99": 9350}, "Creation_Date": {"9999-99-99": 168465}}
T3 Record Status {"Active": 168465}
T3 Exclusion Program {"Reciprocal": 159320, "NonProcurement": 9113, "Procurement": 32}; not Reciprocal and created on or after 1995-08-25: 9145
T3 CT Code filled on rows created after 2012-08-31: 12078
T3 Cross-Reference longer than 200 characters: 4639; longest 3044; filled values not starting "(also": 0
T3 Open Data Flag filled on 0 rows; Blank (Deprecated) filled on 0 rows
T4 LF characters 168691; CR characters 2084; CRLF pairs 7; CSV records including the header 168466; rows with a line break inside a field 1281 {"Additional Comments": 1281}
T5 Classification {"Individual": 133274, "Special Entity Designation": 25585, "Firm": 8284, "Vessel": 1322}; Name filled {"Individual": 0, "Special Entity Designation": 25585, "Firm": 8284, "Vessel": 1322}
T6 Excluding Agency OFAC 41712 {"Special Entity Designation": 23723, "Individual": 16666, "Vessel": 1322, "Firm": 1}; Exclusion Program {"Reciprocal": 41712}; Exclusion Type {"Prohibition/Restriction": 41712}; Exclusion Type values naming OFAC 0
T6 OFAC rows with a CT Code 9609, most common {"(blank)": 32103, "03-SDN-01": 3562, "03-SDGT-01": 3350, "03-SDNTK-01": 1846, "03-FTO-01": 450, "03-SDNT-01": 200, "03-SDT-01": 179, "BPI-SDNTK": 22}; Country XUN 8225 (OFAC 8222); Country blank 634; Excluding Agency blank 2424
T7 Exclusion Type {"Prohibition/Restriction": 135313, "Ineligible (Proceedings Completed)": 29061, "Ineligible (Proceedings Pending)": 2691, "Ineligible (Proceedings Complete)": 1190, "Voluntary Exclusion": 210}
T7 Creation_Date range by Exclusion Type {"Ineligible (Proceedings Completed)": ["1997-02-03", "2026-02-27"], "Prohibition/Restriction": ["1997-02-03", "2026-09-09"], "Ineligible (Proceedings Complete)": ["2026-03-03", "2026-09-16"], "Ineligible (Proceedings Pending)": ["1997-02-03", "2026-09-16"], "Voluntary Exclusion": ["1997-02-03", "2026-09-01"]}
T8 SAM Number distinct 168465 of 168465; rows repeating every other column 4005 in 3864 groups, at most 6 copies; by classification {"Special Entity Designation": 2132, "Individual": 1835, "Firm": 28, "Vessel": 10}; by agency {"OFAC": 3468, "DOJ": 279, "OPM": 231, "HHS": 8}
T8 Unique Entity IDs on more than one row 7346, covering 16600 rows; on rows that differ in more than SAM Number 5089
T9 Unique Entity ID filled {"Individual": 13490, "Special Entity Designation": 25581, "Firm": 7687, "Vessel": 911}; lengths {"12": 47668, "5": 1}; CAGE filled {"Individual": 0, "Special Entity Designation": 0, "Firm": 424, "Vessel": 0}; Cross-Reference filled {"Individual": 19107, "Special Entity Designation": 18505, "Firm": 3027, "Vessel": 193}; Name containing a comma {"Individual": 0, "Special Entity Designation": 2428, "Firm": 3688, "Vessel": 0}
T10 Active Date blank 11016; filled 157449, different from Creation_Date 145279, Creation_Date later 143453
T10 Termination Date Indefinite 159115; a date 9350, before the file date 13
T10 Creation_Date before 2022-04-04 {"Individual": 113450, "Special Entity Designation": 9298, "Firm": 6696, "Vessel": 411}
exit 0
What this does not cover
Whether an exclusion bars a particular award or payment, what to do about a match, OFAC's own lists and obligations, the separate FASCSA orders file and the entity registration extracts, and what SAM.gov's Terms require of any particular use of the data, which is a question for your counsel. This page is about reading the file correctly, not procurement, sanctions or legal advice.
Aervik Labs' Vendor Entity Screening API screens company names against this daily file and trade.gov's Consolidated Screening List. It is not yet open for signups.