SEC Form 4 · implementation
Parsing SEC Form 4 XML: nine traps in EDGAR's filing data
Getting a company's insider trades out of EDGAR looks like three steps: read the company's filing list, keep the Form 4s, parse the XML. Done that way, it returns trades in other companies, misreads flags, and keeps numbers that were later corrected. Each trap below is shown on real filings.
Where the data lives
# a company's recent filings (CIK zero-padded to 10 digits) https://data.sec.gov/submissions/CIK##########.json # one filing's documents https://www.sec.gov/Archives/edgar/data/<CIK>/<accession-no-dashes>/
Both are free. SEC's fair-access
policy asks every script to declare a User-Agent with a company name and contact
address, and sets the current maximum at 10 requests per second.
1. A company's filing list is not a list of its insiders' trades
The submissions file lists every filing the company is a party to — including Form 4s where the
company is the insider, reporting its own stake in someone else. Berkshire Hathaway's list
holds accession 0001193125-26-333151, filed 2026-08-04. Its issuer is DaVita Inc.
Filter on form == "4" alone and that becomes a Berkshire insider trade.
Fix: read <issuerCik> from each filing and keep it only when it
equals the company you asked about.
How often: in 2026 Q2, 35 of the 4,497 companies that were the issuer on at least one Form 4 also appeared as a reporting owner on another company's Form 4 — 80 filings out of 49,832. Rare across the market, and not rare for those companies: for 20 of them, such filings were at least 10% of the quarter's Form 4s listed under their CIK, and for 2 they outnumbered the company's own.
2. primaryDocument points at the rendered page, not the XML
For a Form 4 it reads like xslF345X06/form4.xml. That path serves the
human-readable view. The raw ownershipDocument XML is the same filename one folder
up, directly in the accession directory.
3. A 200 is not proof you got a Form 4
A parser that tolerates unexpected input will turn any non-filing body — an error page, an
interstitial — into a filing with zero transactions, and nothing downstream will notice. Every
Form 4 primary document has an <ownershipDocument> root. Check for it before
parsing, and treat its absence as a failed fetch, not an empty filing.
4. Boolean flags come in two dialects
Relationship flags such as <isDirector> are written true/false
by some filing agents and 1/0 by others — sometimes within one company's
own filings. In the run below, Berkshire's filings write isDirector as 1,
true and false; Tesla's as 1 and 0. A check for
=== "true" silently drops every director who filed through an agent that writes
1.
Measured: in a random sample of 600 of the 49,832 Form 4s filed in 2026 Q2, 414 (69%)
wrote the relationship flags as 1/0 and 186 (31%) as
true/false. Of the 360 that marked a reporting owner as a director, 246 wrote
1 — a check for "true" misses most of them. Across all 49,832, the Rule
10b5-1 checkbox aff10b5One was 0/1 in 77.5%.
5. Joint filings don't tag transactions by filer
One Form 4 can carry several <reportingOwner> elements and a single table of
transactions. The DaVita filing names both Berkshire Hathaway and Warren Buffett. Nothing in the
transaction rows says which owner each row belongs to, so do not attribute every row to the first
owner listed without saying so. In 2026 Q2, 1,022 of the 49,832 Form 4s (2.1%) named more than one
reporting owner.
6. The Rule 10b5-1 signal has two sources, and neither is per row
The form-level checkbox <aff10b5One> says a transaction on the form was made
under a Rule 10b5-1(c) plan; it does not say which one. Row footnotes can say more — in either
direction. SouthState Bank Corp's Form 4/A 0001573461-26-000002 carries a footnote that
transactions "were erroneously reported as being made pursuant to a Rule 10b5-1(c) plan" and
"were not made pursuant to a Rule 10b5-1 plan". Searching footnotes for the text
10b5-1 reads that retraction as a plan trade.
7. Amendments supersede
A Form 4/A corrects an earlier filing. A reader that keeps only form == "4" keeps the
uncorrected numbers and never sees the correction; a reader that keeps both double-counts. Decide
which you do, and tell whoever reads your output. In 2026 Q2, 713 Form 4/A amendments were filed, 1.4%
of Form 4 and 4/A filings combined.
8. transactionTimeliness is not a lateness flag
Each transaction row can carry <transactionTimeliness>, and SEC's specification lists its
values as E (early), L (late) and empty (on time). On a Form 4 it cannot say late.
The EDGAR Ownership XML Technical Specification Version 5.5, March 2026, section 4.3.8.2, says
“By definition, a “4” transaction is on time. Provide no value for this case.” and
“By definition, a “5” transaction is early.” So on a Form 4, empty means on time by
definition, not that the filer said so, and E marks a transaction reportable on Form 5 that was
reported early, which can be long after it happened.
Fix: measure the interval yourself, in business days from the transaction date to the filing
date, and start from <deemedExecutionDate> where a row has one. Measure Form 4/A amendments
separately rather than mixing them in. Even then, an interval longer than two business days is not by itself
a late filing: a plan transaction with a deemed execution date, a Form 5 transaction reported early and a
submission after EDGAR's evening cutoff can each produce one.
How often: in 2026 Q2, 0 of the
107,119 Form 4 transaction rows carried L. A deemed
execution date appeared on 0.9% of rows, and
5,810 of the 6,204 rows filed more than two
business days after their transaction date (93.6%) carried no deemed execution date, no
transaction form type 5 and no timeliness value. The quarter's
672 measured Form 4/A amendments had a median of
13 business days from the earliest transaction date. The rule
text, method and data are in the research page's
filing timing section.
9. A $0 price on an exercise is not a free trade
In Table II, <transactionPricePerShare> is the price of the derivative security itself.
Form 4's instructions say that for an exercise or conversion the filer should "leave column 8 blank and report
the exercise or conversion price of the derivative security in column 2", which is
<conversionOrExercisePrice>. So on those rows the per-transaction price is blank or
0, and shares multiplied by it gives a value of zero even when the exercise price is above zero.
Fix: on a Table II row coded M, X, O or C, treat a
blank or zero <transactionPricePerShare> as not stated, and read
<conversionOrExercisePrice> for the price paid.
How often: in 2026 Q2, 12,172 of the 12,673 Table II rows with those codes on Form 4 originals (96.0%) had a blank or zero per-transaction price, and 3,054 of those carried an exercise or conversion price above zero.
The whole thing, runnable
Node 18 or later, no dependencies. It reads a company's recent Form 4s and handles traps 1 to 4,
printing every reporting owner so joint filings are visible. Set SEC_UA to your own
company name and contact address.
// Recent Form 4 filings for ONE company, with the traps handled. Node 18+ (global fetch).
// SEC fair access: declare who you are, and stay under 10 requests per second.
const UA = process.env.SEC_UA ?? 'Your Company Name admin@example.com';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const get = async (url) => {
const res = await fetch(url, { headers: { 'User-Agent': UA } });
if (!res.ok) throw new Error(`${res.status} ${url}`);
return res;
};
// Boolean flags arrive as "true"/"false" from some filing agents and "1"/"0" from others.
const flag = (xml, tag) => {
const v = xml.match(new RegExp(`<${tag}>\\s*([^<]*?)\\s*</${tag}>`))?.[1]?.toLowerCase();
return v === 'true' || v === '1' ? true : v === 'false' || v === '0' ? false : null;
};
const cik = process.argv[2] ?? '1067983'; // Berkshire Hathaway
const limit = Number(process.argv[3] ?? 6);
const padded = cik.padStart(10, '0');
const subs = await (await get(`https://data.sec.gov/submissions/CIK${padded}.json`)).json();
const r = subs.filings.recent;
let seen = 0;
for (let i = 0; i < r.form.length && seen < limit; i++) {
if (r.form[i] !== '4') continue; // 4/A amendments are a separate decision
seen++;
await sleep(150);
// primaryDocument is the RENDERED view ("xslF345X06/form4.xml"); the raw XML is the same
// filename one folder up, directly in the accession directory.
const file = r.primaryDocument[i].split('/').pop();
const acc = r.accessionNumber[i].replace(/-/g, '');
const xml = await (await get(`https://www.sec.gov/Archives/edgar/data/${Number(cik)}/${acc}/${file}`)).text();
// A 200 is not proof you got a Form 4 -- check the root element before parsing.
if (!/<ownershipDocument[\s>]/.test(xml)) { console.log(`${r.accessionNumber[i]} not a Form 4 document -- skipped`); continue; }
const issuerCik = xml.match(/<issuerCik>\s*0*(\d+)\s*<\/issuerCik>/)?.[1];
const issuerName = xml.match(/<issuerName>\s*([^<]+?)\s*<\/issuerName>/)?.[1];
const owners = [...xml.matchAll(/<rptOwnerName>\s*([^<]+?)\s*<\/rptOwnerName>/g)].map((m) => m[1]);
const raw = xml.match(/<isDirector>\s*([^<]*?)\s*<\/isDirector>/)?.[1];
const roles = ['isDirector', 'isOfficer', 'isTenPercentOwner'].filter((t) => flag(xml, t)).join(',') || 'none';
const keep = issuerCik === String(Number(cik));
console.log(`${r.accessionNumber[i]} ${r.filingDate[i]} issuer: ${issuerName} (CIK ${issuerCik}) owners: ${owners.join('; ')} roles: ${roles} (isDirector written "${raw}") -> ${keep ? 'KEEP' : 'DROP: this company is the insider, not the issuer'}`);
}
Run against live EDGAR on 2026-09-14 (UTC), for Berkshire Hathaway and then Tesla, it printed:
0001728451-26-000003 2026-09-04 issuer: BERKSHIRE HATHAWAY INC (CIK 1067983) owners: Jain Ajit roles: isDirector,isOfficer (isDirector written "1") -> KEEP 0001193125-26-349574 2026-08-13 issuer: BERKSHIRE HATHAWAY INC (CIK 1067983) owners: O'Sullivan Michael J. roles: isOfficer (isDirector written "false") -> KEEP 0001193125-26-349555 2026-08-13 issuer: BERKSHIRE HATHAWAY INC (CIK 1067983) owners: Chang Charles C roles: isOfficer (isDirector written "false") -> KEEP 0001193125-26-333151 2026-08-04 issuer: DAVITA INC. (CIK 927066) owners: BERKSHIRE HATHAWAY INC; BUFFETT WARREN E roles: isTenPercentOwner (isDirector written "false") -> DROP: this company is the insider, not the issuer 0001193125-26-304843 2026-07-15 issuer: BERKSHIRE HATHAWAY INC (CIK 1067983) owners: BUFFETT WARREN E roles: isDirector,isTenPercentOwner (isDirector written "true") -> KEEP 0001104659-26-106432 2026-09-09 issuer: Tesla, Inc. (CIK 1318605) owners: Taneja Vaibhav roles: isOfficer (isDirector written "0") -> KEEP 0001104659-26-075213 2026-06-17 issuer: Tesla, Inc. (CIK 1318605) owners: Musk Elon roles: isDirector,isOfficer,isTenPercentOwner (isDirector written "1") -> KEEP 0001104659-26-071970 2026-06-09 issuer: Tesla, Inc. (CIK 1318605) owners: Taneja Vaibhav roles: isOfficer (isDirector written "0") -> KEEP
What our API does with these
The SEC Form 4 Insider Alerts API checks the issuer before it
reads any transaction. When every Form 4 it examines reports the company as an insider somewhere
else, it answers with a 404 that says so, rather than an empty list. Relationship flags are read in
both dialects. A row is marked as a plan trade only when that row's own footnote places it under a
plan, and a denial outranks an affirmation. Joint filings are attributed to the first-listed owner,
and every response says so. Form 4/A amendments are not read, and every response says that too. On a
derivative row priced at 0, transaction_value_usd is null rather than a computed zero.
How the figures were measured
From SEC's Insider Transactions Data Set for 2026 Q2 (every Form 4 and 4/A in the quarter) and 600 Form 4 XML documents sampled at random from it, read from EDGAR on 2026-09-14. The issuer-as-owner count was computed three independent ways and agreed; the flag spellings were tallied by two separate parsers that agreed on all 600 documents. Limits: it is one quarter; the issuer-as-owner count includes only companies that filed as an issuer that quarter, so it is a floor; and the sample figures carry a margin of about ±4 points. The trap 8 figures come from a second pass over the same data set file, read : business days counted by two independent programs that agreed on every compared value, with the timing fields checked against 30 filings' own submission text. No person is counted, named or ranked in these figures. The method, sources, limits and downloadable data are on the research page: SEC Form 4, 2026 Q2: issuer-as-owner filings, flag spellings and filing timing.
What this does not cover
The submissions file holds a company's recent filings; older ones sit in separate files it points to, which the snippet does not follow. Forms 3 and 5 are not covered. And none of this says what an insider trade means — only how to read one correctly. It is not investment advice.