Skip to content

acceptance_datetime from get_filings() can be hours off: SEC's submissions JSON is not reliably UTC #1501

Closed

Description

@shahar-arbor

Issue Type

  • Data Quality Issue (incorrect financial values, missing data)

Environment

EdgarTools Version: 5.61.1 (main at 7a2a157)
Python Version: 3.12
Operating System: macOS 26.6

Bug Description

What you expected to happen:
acceptance_datetime on a filing from Company(...).get_filings() is the moment SEC accepted the filing. It should agree with filing.header.acceptance_datetime and with the "Accepted" time on SEC's index page.

What actually happened:
The value comes from acceptanceDateTime in data.sec.gov/submissions/CIK##########.json, parsed as UTC because of its trailing Z (edgar/dates.py::parse_acceptance_datetime). That field is not reliably UTC. For some files it is 4 hours late.

Apple 10-Q 0000320193-26-000020, read on 2026-10-08:

Source Value
SEC index page, "Accepted" (ET) 2026-07-31 06:01:02
filing.header.acceptance_datetime (naive ET) 2026-07-31 06:01:02
True UTC 2026-07-31 10:01:02
submissions JSON / get_filings()[i].acceptance_datetime 2026-07-31 14:01:02+00:00

Tesla's 10-Q 0001628280-26-049270 is correct: 21:02:31 ET, reported as 01:02:31 UTC the next day.

Offset of the JSON value from the header's ET time, measured on 2026-10-08 over 3–8 filings per company:

Filers Offset Meaning
Tesla, Microsoft, NVIDIA, SVB, Heliogen, Bed Bath & Beyond, Meta Materials +4 h (EDT) / +5 h (EST) true UTC
Apple, Carvana (every filing sampled, any filer agent) +8 h 4 h late

It also changes over time. On 2026-10-07 the same check gave +4/+5 h (true UTC) for Apple, but 0 h (ET labelled as UTC) for SVB, Heliogen, Bed Bath & Beyond and Meta Materials. So the error is per file and per day. No fixed offset corrects it.

A related problem: EntityFiling.acceptance_datetime is a tz-aware UTC datetime, while filing.header.acceptance_datetime is a naive ET datetime. The same name gives two conventions.

Reproduction

Company/Ticker: AAPL
Relevant Forms: any

Minimal code to reproduce:

from edgar import Company

f = Company("AAPL").get_filings(accession_number="0000320193-26-000020")[0]
f.acceptance_datetime          # 2026-07-31 14:01:02+00:00   (SEC: 10:01:02 UTC)
f.header.acceptance_datetime   # 2026-07-31 06:01:02         (naive ET, correct)

Additional Context

The SGML header (ACCEPTANCE-DATETIME in {accession}-index-headers.html or the .txt submission) is the only source that matched SEC's index page on every filing we checked. That holds for 2015–2026, both DST states, and every filer above.

Possible fixes, for the maintainers to choose:

  • document that the submissions-JSON value is unreliable and point to filing.header.acceptance_datetime; or
  • read the header when a precise time matters (one extra request per filing).

In our own code we moved to the header for this reason.

Activity

  1. arhancanli commented on Oct 11, 2026

    @arhancanli

    Some data that may help pin this down. I measured the same thing on 2026-10-11: 450 filings from 25 companies' submissions files (in each file, the 12 newest plus 6 from December to February), each compared with the "Accepted" time on its filing index page.

    1. The error is per file, and the same for every filing in a file. Each file was either right for all 18 filings, or off by exactly one Eastern offset for all 18: +4 h for filings accepted in EDT, +5 h in EST. That held across forms (4, 8-K, 424B2, 13G, 10-K, 13F-HR ...), filing agents, times of day and acceptance years. No other offset appeared.

    Right (equals true UTC) Shifted by one offset (+4 h EDT / +5 h EST)
    MSFT, GOOGL, XOM, PFE, KO, TSLA, NFLX, INTC, GE, F, O, SPY AAPL, NVDA, AMZN, META, JPM, BAC, WMT, BRK, TSM, GME, PLTR, MRNA, Vanguard (CIK 102909)

    2. The same filing reads differently in two files. Apple's director Form 4s of 2026-02-26 are listed in Apple's file and in each director's own file:

    Accession Apple's file Director's file Index page (ET)
    0001059235-26-000004 (Wagner) 2026-02-27T04:34:19Z 2026-02-26T23:34:19Z 2026-02-26 18:34:19
    0001216519-26-000004 (Sugar) 2026-02-27T04:33:49Z 2026-02-26T23:33:49Z 2026-02-26 18:33:49

    So the value depends on the file it is read from, not on the filing.

    3. In this sample the split follows each file's newest filing.

    • Every file whose newest filing was accepted on or before 2026-10-02 18:13 ET was right.
    • Every file whose newest filing was accepted on or after 2026-10-05 19:17 ET was shifted, including its filings from February.

    That is consistent with files rebuilt since early October being written with the extra offset. I can't see rebuild times directly (data.sec.gov sends no Last-Modified), so treat it as a hypothesis. One way to confirm: re-measure MSFT after its next filing. I saw no Eastern-time-labelled-UTC files today.

    4. One request per file is enough, not one per filing. Because a file is uniform, one index page (or SGML header) calibrates every acceptanceDateTime in it:

    • the file's shift is k = (api - true_utc) / et_offset, one of -1, 0 or +1;
    • each filing's true time is api - k * offset, where the offset is 4 h or 5 h by DST at the true time.

    I'd still check k against a second filing from the other DST period, and fall back to per-filing headers if the two disagree. I'd also recalibrate whenever the file is re-fetched, since a rebuild can change it.

    We hit this in our own event-study tool (day 0 depends on whether a filing came before or after the 16:00 close) and now calibrate per file this way: arhancanli/canlicapital#461 (MIT; about 40 lines in acceptanceTimes). If a calibration like this would be welcome in edgartools, say as an opt-in correction in EntityFilings or a helper next to parse_acceptance_datetime, I'm happy to open a PR.

    Reproduction for point 2, standard library only:

    import json, re, urllib.request
    UA = {"User-Agent": "Your Name [email protected]"}
    get = lambda u: urllib.request.urlopen(urllib.request.Request(u, headers=UA)).read().decode()
    acc = "0001059235-26-000004"
    for cik in (320193, 1059235):  # Apple's file, then the director's (Susan Wagner)
        r = json.loads(get(f"https://data.sec.gov/submissions/CIK{cik:010d}.json"))["filings"]["recent"]
        print(cik, r["acceptanceDateTime"][r["accessionNumber"].index(acc)])
    html = get(f"https://www.sec.gov/Archives/edgar/data/320193/{acc.replace('-', '')}/{acc}-index.htm")
    print("index:", re.search(r'Accepted</div>\s*<div class="info">([^<]+)', html).group(1))
    # 320193 2026-02-27T04:34:19.000Z
    # 1059235 2026-02-26T23:34:19.000Z
    # index: 2026-02-26 18:34:19
  2. shahar-arbor commented on Oct 13, 2026

    @shahar-arbor
    ContributorAuthor

    Confirming the per-file rule on 2026-10-12: Apple's recent file and its 2015 paginated file (CIK0000320193-submissions-001.json) are both +8 h from the header (k=+1 in your terms) on 9/9 sampled filings each; Tesla's two files are true UTC on 9/9 each.

    One exception that matters for the calibration step: on 2026-10-08, about 16 minutes after Apple's Form 4 0001140361-26-039158 was accepted (18:30:32 ET), Apple's file listed it at 2026-10-08T22:30:32Z (true UTC) while the rest of the file was +8 h. It now reads 2026-10-09T02:30:32Z, like the rest. So a filing appended since the file's last rebuild can carry a different k than the file it sits in, and calibrating from the newest filing right after it lands can give the wrong k for the others. Your second-filing check catches this.

    Also, on 2026-09-30 whole files for MMAT, SVB, Heliogen and Bed Bath read as Eastern wall time labelled Z (k=-1), so all three states occur.

  3. arhancanli commented on Oct 13, 2026

    @arhancanli

    Thanks, this is the piece I was missing, and it changes the calibration rule.

    I just re-read Apple's file: 0001140361-26-039158 now reads 2026-10-09T02:30:32.000Z, so it matches what you saw after the rebuild.

    Two consequences for anyone calibrating:

    1. Your Form 4 case fits the rebuild hypothesis from my earlier comment: a filing appended after the last rebuild carries true UTC, and the next rebuild rewrites the whole file with the extra offset. That means the newest filings in a file are the worst ones to calibrate from. A safer rule is to take k from a filing that is a few days old, require two or three filings from different days to agree, and fall back to per-filing headers when they don't.
    2. If a file can hold two states at once (rebuilt part plus a freshly appended filing), then "k is uniform per file" is only safe for filings older than the last rebuild. I only measured files in a single state (18 of 18 each), so I can't say how long the mixed window lasts. Your 16-minute observation is the only data point on it.

    The k=-1 files (Eastern wall time labelled Z) are new to me; I saw none on 2026-10-11, and I haven't re-checked MMAT, SVB, Heliogen or Bed Bath, so I'm taking those from your measurement. A calibration that only tries k in {0, +1} would silently mis-time those, so the helper should test all three values and refuse to guess when none fits.

    If it's useful I can fold the mixed-state case into the PR proposal for edgartools: calibrate from older filings, check agreement across at least two, and treat any filing newer than the last disagreement as unverified. Disclosure: I maintain the event-study tool linked above.

  4. MaxHaro commented on Oct 15, 2026

    @MaxHaro
    Contributor

    Thanks @arhancanli and @shahar-arbor for the careful measurements. The per-file finding and the Form 4 exception are very useful.

    @arhancanli, do you still plan to open the PR? No rush. I'm happy to leave it with you, help with tests or review, or pick it up if you'd rather hand it over.

    @dgunning, I saw #1502 documents the caveat and leaves the value change for 6.0. One question before anyone writes code: would an opt-in correction be welcome in 5.x, with default output unchanged, or would you rather wait for 6.0? If it's welcome, do you prefer a method on EntityFilings or a keyword on get_filings()?

    The sketch I have in mind, default behavior unchanged:

    • EntityFilings.verify_acceptance() returns a copy with corrected times.
    • Per submissions file, read 2-3 headers from older filings on different days and look for k in {-1, 0, +1} where JSON minus header UTC equals k times the Eastern offset. The newest filing is checked too.
    • If no k fits or samples disagree, rows are not guessed. They come back as None, or read from filing.header.acceptance_datetime on request.
    • Cost is a few requests per file, not one per filing. Tests use recorded cassettes.
  5. arhancanli commented on Oct 15, 2026

    @arhancanli

    Thanks @MaxHaro, and happy to have you on it. I'd rather not promise a date for the PR from my side: I'd like @dgunning's answer on 5.x opt-in versus 6.0 first, since that decides the shape (a method versus a keyword) and whether it is worth writing now. If it is welcome, I'm glad to review, add test cases from my measurements, or hand over the data. If you want to start before that, please don't wait for me.

    Your sketch matches what we measured. Three points from the data above that I'd put into the tests:

    1. Treat the newest filings separately. shahar-arbor's Apple Form 4 (0001140361-26-039158) was true UTC for a while inside a file whose other rows were +8 h. So "also check the newest filing" is right, but it should be checked against its own header and never used to pick k for the file.
    2. Compute the offset from the true time, not the JSON time. Near a DST change, k * offset uses 4 h or 5 h depending on which side the true acceptance time falls on. A fixture with one filing on each side (for example one in July and one in February from the same shifted file) catches an implementation that picks the offset from the shifted value.
    3. A fixture for each state. Right (MSFT, TSLA), shifted +1 (AAPL, NVDA) and k=-1 (the Eastern-time-labelled-Z files shahar-arbor saw on 2026-09-30), plus a "nothing fits" case that must return None rather than a guess.

    One caveat on the sample: all of it is from 2026-10-07 to 2026-10-13, and the states have changed between days, so cassettes should record the fetch date.

  6. dgunning commented on Oct 21, 2026

    @dgunning
    Owner

    Thanks @shahar-arbor, @arhancanli and @MaxHaro. Your measurements settled most of this. Here is the direction, plus one new data point.

    New data point (2026-10-21): Tesla's file has flipped. 0001628280-26-049270 was accepted at 21:02:31 ET (01:02:31 UTC on 07-23), and get_filings() now returns 2026-07-23 05:02:31+00:00. That is +4 h, although it was correct when this issue was opened. Apple's 10-Q 0000320193-26-000020 is still +4 h (14:01:02 instead of 10:01:02 UTC), and its Form 4 0001140361-26-039158 now reads 02:30:32Z. So the offset follows each rebuild of a company's file, not just the company.

    What we won't build: a per-file offset correction. The rule changes with each rebuild, and the newest filings are exceptions to it. A correction that is right today and wrong after the next rebuild is worse than an honestly labelled value, especially for backtests, which need the same answer every run.

    What we'd welcome for 6.1 (all additive, so it doesn't need to race the 6.0 cutoff):

    Piece Design
    Exact time, one filing filing.accepted_at on every Filing. Read it from {accession}-index-headers.html, cached permanently because archive files never change. Return a tz-aware America/New_York datetime, SEC's own clock and the one market sessions use.
    Exact time, in bulk filings.accepted_at(), which returns the exact values for a whole Filings. It is rate-limited and shows progress, so the cost is explicit, not hidden in an attribute read inside a loop.
    The JSON value Keep it, renamed reported_acceptance_datetime (and the to_pandas() column), with the caveat in the docstring.
    EntityFiling.acceptance_datetime Deprecated in 6.x, removed in 7.0. filing.header.acceptance_datetime stays as the raw SGML header value.

    Cost of the exact value, measured on 2026-10-21 with the cache bypassed. All three sources agreed on the acceptance time for every filing:

    Filing full .txt (what filing.header downloads today) -index-headers.html
    Apple 10-Q 5,807 KB, 348 ms 15 KB, 168 ms
    Tesla 10-Q 8,618 KB 19 KB, 258 ms
    Apple Form 4 5 KB 3 KB, 174 ms
    Caterpillar 8-K 0001104659-26-116198 284 KB 6 KB, 67 ms

    That is one small request per filing. At SEC's 9 requests per second, 1,000 filings take about 2 minutes the first time and are cached after that. It is also far lighter than the current filing.header workaround, which downloads the whole submission.

    @MaxHaro, the implementation is yours if you'd like it. @arhancanli, your per-file measurements and the Form 4 rebuild case would make good test cases. Please treat a filing appended since the last rebuild as a test of reported_acceptance_datetime's caveat, not as something to correct. One PR is fine; please include a ground-truth assertion for one filing whose Accepted time you read from its index page.

  7. arhancanli commented on Oct 21, 2026

    @arhancanli

    Thanks @dgunning. The direction is right, and I agree with not shipping a per-file correction. I did build one for our event-study tool; it needs a rule for the mixed state (a filing appended after a rebuild), and that rule would also change after the next rebuild.

    I re-checked your two examples just now (2026-10-22), and both match your numbers:

    Accession submissions file -index-headers.html ACCEPTANCE-DATETIME -index.htm Accepted (ET)
    Tesla 0001628280-26-049270 2026-07-23T05:02:31Z 20260722210231 2026-07-22 21:02:31
    Apple 0000320193-26-000020 2026-07-31T14:01:02Z 20260731060102 2026-07-31 06:01:02

    Both are +4 h against true UTC. Tesla was one of the files I measured as right on 2026-10-11, so the flip fits the rebuild picture.

    Two test-case suggestions for the PR:

    1. Pin the ground truth, not the JSON. The assertion should compare filing.accepted_at to the -index.htm "Accepted" value converted from America/New_York, and record the fetch date. The JSON value is the one that moves.
    2. A DST pair from one file. One summer filing (+4 h) and one winter filing (+5 h) read from the same file. reported_acceptance_datetime can then be tested as "differs from accepted_at by the offset of the true time", which catches a helper that derives the offset from the shifted value.

    @MaxHaro, if it helps, I'll put the 450-filing table (accession, file value, index-page time) into a gist as a fixture source, with the fetch date on every row. Say so here and I'll link it.

  8. lizhuojunx86 commented on Oct 27, 2026

    @lizhuojunx86

    One more data point on the third state (Eastern wall time labelled Z), from an earnings-timing audit I ran:

    • In September 2026 I compared 228 acceptanceDateTime values with their index pages. 185 were true UTC and 43 were Eastern wall time labelled Z. All 43 came from issuers that are no longer in company_tickers.json (delisted or acquired), which fits the MMAT, SVB, Heliogen and Bed Bath files reported above for 2026-09-30.
    • On 2026-10-27 I rechecked seven of the 43. All seven now read as true UTC.

    So files of delisted issuers move between states too, which is one more reason to read accepted_at from -index-headers.html rather than correct the JSON.

    For anyone timing earnings releases before 6.1 lands: earnings-session (https://github.com/lizhuojunx86/earnings-session, MIT) reads the "Accepted" time from the index page, keeps the JSON value next to it, and records which convention the JSON used for each filing (utc, et_as_utc or mismatch). Disclosure: I maintain it.

  9. arhancanli commented on Oct 27, 2026

    @arhancanli

    Thanks @lizhuojunx86, the re-check of seven files is the useful part: it shows the state follows each rebuild of a file, not the issuer, and a delisted issuer is no more stable than a listed one. That fits dgunning's Tesla and Apple examples above and is one more reason to pin tests to the index-page time rather than to any value read from the JSON.

    One question about the 43: when you rechecked on 2026-10-27, were the seven still absent from company_tickers.json, or had any come back? If they were still delisted and flipped anyway, the flip is tied to the rebuild itself rather than to the ticker list, and a test fixture would have to assume that any file can change state between runs.

  10. lizhuojunx86 commented on Oct 28, 2026

    @lizhuojunx86

    They were still absent. Instead of the seven, I reran all 43 on 2026-10-28 (00:12 UTC): every acceptanceDateTime now matches the index-page time as true UTC, and none of the 43 filer CIKs is in that day's company_tickers.json. QDEL and VNOM trade again under new CIKs (1906324 and 2074176) after reorganizations, but the CIKs that filed these 8-Ks are still off the list.

    So all 43 changed convention while staying off the ticker list, which fits the rebuild explanation. For a fixture I'd assert only against the index-page time and let the JSON convention vary between runs.

  11. arhancanli commented on Oct 28, 2026

    @arhancanli

    Thanks, rerunning all 43 settles it: they flipped together while staying off the ticker list, so the state belongs to the file's last rebuild, not to the issuer's status. It also means a fixture that pins the JSON convention will pass one day and fail the next without any code change, so asserting on the index-page time alone is the stable choice.

    One thing that would pin the cause down: does earnings-session keep the response's Last-Modified or ETag next to the utc / et_as_utc label? If all 43 changed between your 2026-10-27 and 10-28 fetches, the headers should show a single rebuild window, and that would tell dgunning whether this is one batch job or several.

  12. lizhuojunx86 commented on Oct 28, 2026

    @lizhuojunx86

    No. It caches the JSON body but not the headers, and SEC doesn't send either one for these files: data.sec.gov answers HEAD with 403, and a GET of a submissions file returns Date and request IDs but no Last-Modified or ETag (I checked all 35 filers today). So the only bound I have is my own fetch log: all 43 read as Eastern wall time on 2026-09-17, 7 of 7 read as UTC on 2026-10-27, and 43 of 43 on 10-28. A single rebuild somewhere in those six weeks is the simplest reading, but I can't date it more closely from outside.

  13. arhancanli commented on Oct 28, 2026

    @arhancanli

    Thanks @lizhuojunx86, checking all 35 filers for Last-Modified and ETag and finding neither closes that route, and the fetch log is a fair bound: a rebuild some time between 2026-09-17 and 10-27, with the 7 of 7 and 43 of 43 reads agreeing.

    One thing your log could separate from "a single rebuild": direction. The 185 files that were true UTC in September, have any of them gone the other way and read as Eastern wall time labelled Z since? If the 43 only ever moved from et_as_utc to utc and none of the 185 moved back, it looks like a one-way correction applied to the stale files. If some of the 185 flipped the other way, the files are being regenerated by a process that can produce either convention, and a fixture should expect both on any given day.

  14. lizhuojunx86 commented on Oct 28, 2026

    @lizhuojunx86

    None of the 185 went the other way. I re-read all 228 on 2026-10-28 (12:52 UTC): the 185 that were true UTC in September still are, and the 43 still read as true UTC, so in this sample the change was one-way.

    The bigger change is in the files reported above as shifted. At 12:55 UTC I checked the 13 files listed as +1 earlier in this thread (AAPL, NVDA, AMZN, META, JPM, BAC, WMT, BRK, TSM, GME, PLTR, MRNA, Vanguard), plus MSFT, TSLA and GOOGL, against -index-headers.html. Three filings per file: the newest, the newest accepted before 2026-10-23, and one from February. All 48 read as true UTC, on both sides of DST. The examples from this thread:

    Accession Reported here 2026-10-28 True UTC
    Apple 10-Q 0000320193-26-000020 14:01:02Z 10:01:02Z 10:01:02
    Tesla 0001628280-26-049270 05:02:31Z 01:02:31Z 01:02:31
    Apple Form 4 0001140361-26-039158 2026-10-09 02:30:32Z 2026-10-08 22:30:32Z 22:30:32

    So all three states seen in this thread have converged on true UTC since 2026-10-22, the last day a +1 file was confirmed here. I can't tell from outside whether this is a fix or one more rebuild, so I'd keep the 6.1 design as planned. If it holds, the caveat in #1502 could name the affected period, since anyone who cached submissions JSON before late October still has the shifted values.

    I can share the per-filing table if it's useful for fixtures.

  15. arhancanli commented on Oct 28, 2026

    @arhancanli

    Thanks @lizhuojunx86, 48 of 48 on both sides of DST, taken from -index-headers.html rather than the JSON, is the strongest evidence in this thread, and one-way across 228 files is a useful result. It also fits the one-time-correction reading better than the "process that produces either convention" one.

    Agreed on the caveat in #1502 naming the affected period. Anyone who cached submissions JSON before about 2026-10-22 keeps the shifted values, so a cache key that includes a fetch date, or a re-validation against the index headers for filings accepted before that date, is the practical advice.

    One question for your fixtures: do your per-filing tables include the pre-2026-10-23 filings whose cached copies would still be shifted, or only the fresh reads? A fixture with both the old (et_as_utc) and new value for the same accession would let the 6.1 parser assert that it normalises either one to the same UTC instant. The per-filing table would be useful for that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    post-6.0Triaged; scheduled after the 6.0 releasews:api-ergonomicsPost-6.0 workstream

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions