Quartet / Quintet

Publishing to dev.to Through Its API — What I Measured

Everything here was observed while publishing and updating articles on a real account, not read somewhere. Dates are given so you can judge how stale it is.

Measured between 2026-09-04 and 2026-09-06, across 19 articles and roughly 40 update calls.

Rate limiting returns 500, not 429

This is the one that costs the most time, because a 500 reads as "the server is broken" and invites a retry loop.

2026-09-04. Nine consecutive PUT /articles/:id calls with no delay:

3 of 9 -> HTTP 500

Nothing in the body says "rate limit". The same requests, replayed with a delay, all succeeded.

spacing calls failures
none 9 3
10 seconds 6 0
10 seconds 11 0
20 seconds 7 0

Ten seconds between write calls was enough across every batch I ran. If you get a 500 on a write, wait before assuming the payload is at fault.

The cover image field has a different name on the way in and out

You set it as main_image. You read it back as cover_image.

request   { "article": { "main_image": "https://example.github.io/covers/a.png" } }
response  { "cover_image": "https://media2.dev.to/dynamic/image/width=1000,.../https%3A%2F%2F..." }

The response URL is dev.to's own CDN, rewritten from the one you sent. That matters: once dev.to has ingested the image, your host going down does not break the article.

If you check for main_image on the response, it is null, and it looks like the write did nothing.

published is taken from the request, every time

There is no "leave it as it is". Whatever you send is what the article becomes.

PUT /articles/:id  with published: false   ->  a live article is taken down

The URL survives, the page 404s, and links to it die. If your tool decides published from a command-line flag, an update without that flag unpublishes the article you meant to edit.

The safe shape is to refuse rather than guess:

if (existing?.published && !publish) {
  throw new Error("This article is published. Updating without --publish takes it down.");
}

Guessing the intent is worse, because "meant to unpublish" and "forgot the flag" look identical from inside the tool.

The article detail response omits series

You can set series on the article object, and it works — but GET /articles/:id does not return it.

I set a series on seven articles and checked the API. Nothing came back. The series box was rendering on the article page the whole time.

# does not tell you whether the series was set
curl -H "api-key: $KEY" https://dev.to/api/articles/4570263 | jq .series

# does
curl -s https://dev.to/<user>/<slug> | grep -o "<series name>"

If you want to know what is displayed, look at what is displayed.

Publishing several articles on one day buries most of them

Not an API behaviour, but it is the mistake the API makes easy.

2026-09-04. Nine English articles published within one day. Page views after 24 hours:

article views
1st 27
2nd 23
3rd 13
4th 10
remaining 5 0

The feed is chronological, so articles published together compete with each other and the later ones are never seen. One a day, from then on.

The ones that sat at zero views did not recover on their own.

Identity: use the id, not the title

GET /articles/me/published gives you your articles. If you find the existing one by title, the day you fix a title-rendering bug is the day your tool stops recognising its own articles and creates duplicates.

Keep a file mapping your filename to the id the API assigned:

{ "ids": { "composite-github-action": 4576296 } }

And stop when the id is recorded but not found. Silently falling through to create is the worst option: you end up with two published copies of the same article competing for the same search terms.

Notes

The workflow itself is available

Quartet, the four-persona version, is published free under MIT. Quintet adds a UI Designer persona, review criteria, a per-Issue parallel execution script, and a 10-chapter guide.

See the free version Product page