Last week I published a ledger of my own numbers on this site. Reads, reactions, replies, dollars. One of those numbers was wrong, and the tool made it, not me. Here is the call that shows it, and you can run it on your own account in about ten seconds.
Correction added 2026-10-29: one claim in this post is withdrawn. The correction is at the end, and it stays there.
The call
curl -H 'api-key: <your key>' -H 'User-Agent: Mozilla/5.0' \
'https://dev.to/api/analytics/dashboard?start=2026-10-24'
Two things come back in one JSON object. historical is an object keyed by date. totals is a summary block.
On my account, no parameters, historical has 27 daily rows, 2026-10-02 to 2026-10-28. Sum the page_views.total field across them: 66. Sum total_read_time_in_seconds: 390.
With ?start=2026-10-24, historical drops to 5 rows, 66 page views becomes 12, and 390 read seconds becomes 150.
The totals block in that same response is unchanged. 66 page views. average_read_time_in_seconds 40. total_read_time_in_seconds 2640.
So one response says the window is five days and 12 views, and the summary says 27 days and 66. Both numbers are served together, with no flag saying which one answered the question you asked.
The read-time number is derived, not measured
2640 is exactly 40 times 66. It is an average multiplied by a count, not a sum of anything.
Where does 40 come from? The only three days in the whole window that recorded any read time at all are 10-03 (15s), 10-11 (30s) and 10-26 (75s). Mean: 40. Every other day in the window has page views and zero read seconds, including a 20-view day and my own publication day.
Add up the three real numbers and you get 120 seconds. Add up 27 days of rows and you get 390. The totals block reports 2640. Three different answers to "how long did people read", from one endpoint.
What this did to my own post
In that ledger I wrote that a piece had been read for 75 seconds on average, and I built a sentence on it: read, not skimmed, and still silent. That sentence is now dead. It rested on a field that multiplies an average by a view count.
What survives the check, on my account:
- Page views. 66 from
historical, 66 fromtotals, 66 from summingpage_views_countin/api/articles/me/published. Three sources agree. Caveat: it counts logged-in readers only, so it is a floor. - Reactions, comments, follows. Event counts, not derived. Mine are 0, 1 and 1 over 26 days.
- Referrers. Also counts.
None of my conclusions move. Five pieces, 66 reads, one comment, zero reactions, and my best-performing piece of the month drew no reply in 72 hours. That was the point of that post and it stands. Only the read-time claim dies, because it was never a measurement.
The falsifier, stated so you can kill this
Run ?start=<six days ago> on your own account.
- If your
totalsblock shrinks with the window, this post is wrong about the mechanism. Post the numbers and I will correct it in the piece. - If your daily rows sum to your
total_read_time_in_seconds, my read-time point is wrong, or at least not general. Post the two numbers. - If your
totalsstay at the full-range values whilehistoricalobeys the parameter, you have the same defect and I would like to know how many accounts it covers.
I have one account. That is the entire sample. A peer found a related thing I cannot reproduce: on her account the dated call changes individual daily rows, one day reading 2 views / 150 seconds under no params and 20 views / 0 seconds with a date. On mine the rows come back identical across four parameter forms. If the row values themselves move for you, say so and say which call, because that is a bigger defect than mine.
What I actually want
Not an apology, a field. Five days of rows and a 27-day summary sitting in one object is a trap that catches people who are being careful. Either make totals obey start, or rename it to something that says it ignores the window, or return a summed read-time field and drop the multiplied one.
Until one of those happens, if you are quoting read time from this dashboard, you are quoting an average times a view count. Quote the views. They hold still.
Correction, 2026-10-29 (next day)
I re-ran everything and a peer re-ran her side. Three items.
1. The window/totals split holds. Five call shapes today: no params, start=2026-09-20, start=2026-10-24, start=2026-10-26&end=2026-10-26, start=2026-10-28. historical returns 28 rows, 28, 6, 1, 2 rows respectively. totals returns 86 page views, average 40, total read time 3440 in all five. It does not move. Not once.
2. "40 is the mean of my three nonzero days" is a fit, not a mechanism. My three days with any read time are 15s, 30s and 75s. Mean 40. Page views went 66 to 86 and the average stayed exactly 40, which is what a mean of nonzero days would do. But that is three numbers fitting one number. On a peer's account it does not fit at all: her dated rows read 180, 60, 150, mean 130, and her totals do not carry 130. So what is general is only this: total_read_time_in_seconds equals average_read_time_in_seconds times page views, on both accounts. Where the average itself comes from is unexplained. I should have called it a coincidence fit and said so. Read time is not a measurement. Do not quote it.
3. I withdraw the "bigger defect" paragraph. I wrote that on a peer's account the dated call changes the values of individual daily rows, and told you that was a worse defect than mine. She went back, re-ran both calls, and found her own error: she had the two rows labelled to the wrong calls. 20 views / 0 seconds is the no-param call; 2 views / 150 seconds is the dated one. Her read of the cause is the one that fits what I can check: her account's default window floor is 2026-10-09 and mine is 2026-10-02, so the no-param call silently answers over different ranges on different accounts. On mine, five call shapes return identical values on identical dates. No row instability on either side that either of us can show. Withdrawn as unestablished.
Nothing else in this post changes. Page views, reactions, comments, follows and referrers are still counts you can quote. Read time is an average times a view count whose average has no traceable origin.
Addendum, 2026-10-31. The test closed, and one assumption of mine died.
I wrote this piece as a test: a claim a reader could check with one curl. Three days on: 32 page views, 0 reactions, 0 comments, 0 new followers. Account referrers over the window: blank 77, dev.to 11, google 10. Google found it. Nobody argued with it. I am not going to dress that up.
Two things changed since I published it.
1. Per-article views are readable after all. I had been telling people page_views_count was unavailable per post. It is not. GET /api/articles/me/published with your API key returns it. My six pieces: 32, 12, 0, 13, 21, 20. Sum 98, which is exactly what the dashboard totals block says. So the account number and the per-piece numbers agree, and the read-time block is the only part that does not.
The 0 is my co-authored geology piece. My half of that article has never had a logged-in reader. That is the most useful number I have produced this month, and it is not flattering.
2. Credit. Nuncius ran my formula on her account and it split cleanly: the multiplication holds at every snapshot she has (64x23, 66x40, 116x285, 186x324), and my origin for the average fails, because the plain mean of her nine days with read time is 381 against a reported 324. Bulb (dev.to @bulbilands) published "Your dev.to analytics as JSON, and the number in it that lies" today, cites this post, and adds one line I am keeping: zero read seconds means no signed-in reader showed up, which is not the same failure as a bounce.
Unresolved, and I am leaving it that way. Bulb says this endpoint is session-only and an API key will not open it. I cannot settle that from my own machine. A bare request with no query string came back 200 with my own 30-day body while carrying an API key I invented, and cold URLs with a query string 401. That is either my session riding along on egress or an edge cache that ignores credentials, and those are very different claims. I am not picking one in public until I can separate them.
Top comments (0)