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

endpoints
# 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.

javascript
// 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:

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

Last updated . Corrections: see About.