DEV Community

Julie
Julie

Posted on Originally published at juliecodestack.github.io

How to publish posts via the dev.to API

Includes a Python script and curl examples

Every time I copied a post into the dev.to editor, added tags and cleaned up the formatting, I'd wonder whether there was a way to automate it. If you have the same question, this article is for you.

The answer: the dev.to API.

In this post I'll show you two ways to use it. First a curl command, so you'll see the exact request the API expects. Then a Python script that automates it. I'll also cover the one thing that trips people up: images.

Get your dev.to API key

First you'll need an API key.

In your dev.to account, go to Settings → Extensions → DEV Community API Keys. Give your key a name and click Generate API Key.

Then expand the Active API Keys list below the button and click the arrow next to your key to reveal it.

Don't paste the key into your source code. We'll load it from an environment variable in the Python version.

Approach 1 (starter example): curl from the command line

Let's start with a simple example, so you can see the HTTP request behind the whole thing.

Open a terminal (on Windows, you may use Git Bash), and run the command below, replacing YOUR_KEY with your own API key.

curl -X POST https://dev.to/api/articles \
  -H "api-key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "article": {
      "title": "My First Post",
      "body_markdown": "# Hello\n\nThis is my post.",
      "tags": ["python", "api"],
      "published": false      
    }
  }'
Enter fullscreen mode Exit fullscreen mode

Here is what each part does:

  • curl -X POST tells the API to create a new article.
  • -H adds a header. One header carries your api-key; the other says the request body is JSON.
  • -d is the body, in JSON format.

Note: published is false (the default), so the post is created as a draft, which you can see in your Dashboard. You can open it, check the formatting, edit it, and publish when you're ready. Set it to true and the post goes live immediately.

The downside: you have to type or paste the API key, title, tags, and body into the terminal every time. That's exactly the busywork we were trying to avoid, so let's use a script to do it. Python is a good choice.

Approach 2 (recommended): a Python script

If you write your posts in Markdown, you probably already have front matter at the top of each file, and you can reuse it.

If you haven't used front matter before, it's simple. It looks like this:

---
title: "My First Post"
tags: [python, api]
---
Enter fullscreen mode Exit fullscreen mode

Just add these few lines to the top of your Markdown file. Make sure there is a space after each colon, as YAML won't parse title:My First Post. Also note that dev.to accepts at most four tags per post.

First, install two packages: requests to talk to the API, and python-frontmatter to read the front matter out of your post. Run this in your terminal:

pip install requests python-frontmatter
Enter fullscreen mode Exit fullscreen mode

Then save the code below as a Python file. Point frontmatter.load() at your post, and set your key as an environment variable (export DEVTO_API_KEY="your_key" in Git Bash, or $env:DEVTO_API_KEY="your_key" in PowerShell).

import os

import frontmatter
import requests

URL = "https://dev.to/api/articles"
API_KEY = os.environ["DEVTO_API_KEY"]

post = frontmatter.load("posts/my-post.md")

article = {
    "article": {
        "title": post["title"],
        "body_markdown": post.content,
        "tags": post.get("tags", []),
        # Optional: if the same post also lives on your own blog
        # "canonical_url": "https://your-blog.example.com/my-post",
        "published": False
    }
}

response = requests.post(
    URL,
    json=article,
    headers={"api-key": API_KEY, "Content-Type": "application/json"}
)

if response.status_code == 201:
    print("Draft Created:", response.json()["url"])
else:
    print("Failed:", response.status_code, response.text)
Enter fullscreen mode Exit fullscreen mode

Comparing requests.post() with the curl command, you'll find that they send the same three things: a URL, headers, and a JSON body. Both are plain HTTP requests. The script just saves you from typing one out every time.

About images

If your Markdown points at local images like ./imgs/screenshot.png, those links break on dev.to. The post has no access to your disk, so you'd have to upload each image by hand.

The fix is to use image URLs that are publicly accessible — from your own blog, GitHub, or another image host. When you publish through the API, dev.to fetches each image and stores its own copy, so the post looks the same there as it does at home.

Note: The URL has to be public. Images behind a login or on a private network won't load.

Wrapping up

Publishing through the API turns a five-minute copy-paste into one command. A few tips:

  • If you also publish the posts to your blog, set canonical_url so search engines know where it was first published.
  • For all the parameters of the API, you may refer to the API documentation.
  • Keep your API key in an environment variable, and never commit it to a public repo.

How do you publish to dev.to — via the API, the editor, or something else? I'd like to hear what's worked for you.

Special thanks to @pascal_cescato_692b7a8a20, whose great post and the detailed explanation in its comments gave me the push to try the dev.to API.

Top comments (13)

Collapse
 
francistrdev profile image
FrancisTRᴅᴇᴠ •

Very Nice guide Julie! It's short and very understandable :D

I tend to post on the dev.to "create post". If you are using the dev.to API, it's important to note to make sure no one has access to the API keys you have. The reason why I don't use dev.to api to post is mainly of fear of leaking my own API key lol. I only use it to display my posts in a friendly format through my portfolio. Creativity is always fun :)

If anything, I think a good project is making your own dev.to editor with the dev.to api. I can imagine it looks not only nice, but very convenient as well. I think Sloan uses the dev.to API to automatically send Welcome Threads, but @ben can correct me if I am wrong.

Well done!

Collapse
 
julies1 profile image
Julie •

Thank you, Francis!😄 I’m glad it’s easy to follow.

So you don't use the dev.to API to post because you're worried about leaking your API key? You seem very security-conscious — I like that.😄

Are the articles on your website linked to dev.to via the API? It's wonderful. When I first saw your website, I was impressed by that, and I wondered how you did it.

As for building my own dev.to editor using the API, I'm afraid it wouldn't be as convenient or full-featured as dev.to’s built-in editor.

Collapse
 
merbayerp profile image
Mustafa ERBAY •

Nice and practical guide, Julie! 😊

I particularly like the Markdown front matter approach. It makes publishing much easier to automate.

One small suggestion: since published is set to False, the success message could say “Draft created” instead of “Published.”

It might also be worth mentioning duplicate prevention. Running the script twice creates two separate articles unless the existing article ID is tracked and updates use the appropriate API endpoint.

Thanks for sharing such a beginner-friendly walkthrough!

Picked as gem
Collapse
 
julies1 profile image
Julie • • Edited

Thank you, Mustafa!😄I’m glad you like it. I also prefer the Markdown front matter approach, since typing or pasting the entire body on the command line is too much work.

Thanks for the suggestion. You’re right: since it is a draft, it would be better to set the success message to either “Draft created” or “Succeeded”. I've made the change in the post.

By “duplication prevention,” do you mean we should avoid running the script more than once on the same article when updating it? Yes, that's right. While testing the script, I ran it twice, and two drafts of the same article appeared in the dashboard.

Collapse
 
merbayerp profile image
Mustafa ERBAY •

Exactly, Julie! 😄 That’s precisely what I meant.

When we run the script again for the same Markdown file, it creates another draft instead of updating the existing one. As you noticed during testing, it’s surprisingly easy to end up with duplicates. 😅

One simple approach would be to save the article ID returned by the API after the first successful creation. On subsequent runs, the script could use that ID to update the existing article rather than creating a new one.

Of course, that’s probably a topic for a follow-up article. Your current guide does a great job of keeping things simple and approachable for beginners.

And thank you for taking the time to update the success message! I really appreciate how open you are to feedback. That’s one of my favorite things about this community. 😊❤️

Looking forward to your next post!

Thread Thread
 
julies1 profile image
Julie •

Mustafa, thank you for pointing this out! 😊You took these cases into account and made the post more complete.

I found that the API has a PUT method for updating a published article by its ID Forem API V1 | Forem Docs, but I didn’t know where to get that ID.

Collapse
 
vaibhav_srivastava_f543ba profile image
Vaibhav Srivastava •

Really clear walkthrough! Using the API for cross-posting developer notes and engineering logs directly from automated pipelines saves so much manual context switching. The tip on structuring frontmatter tags cleanly is super helpful.

Collapse
 
julies1 profile image
Julie •

Thank you! 😄I’m glad it’s helpful. Automation makes publishing posts easier.

Collapse
 
sinarezaei profile image
Sina Rezaei •

A more subtle failure shows up when publishing versioned technical documentation at scale. Imagine generating DEV articles from a repository covering a distributed system: an article for v2.4 references an API contract that changed in v2.5, while its code samples still compile against the newer SDK. The Markdown is valid, the API accepts the post, but the technical explanation is now internally inconsistent. I'd validate code samples and version-specific references against the target release before creating the article, then publish only after those checks pass. Otherwise, automation can scale outdated technical guidance just as efficiently as accurate content.

Collapse
 
julies1 profile image
Julie •

Yes, we need to keep a human in the loop. I set published to false, so the post is saved as a draft on dev.to, and you can check it again before publishing.

Collapse
 
sinarezaei profile image
Sina Rezaei •

That helps with the publishing side, but I’d still separate human review from technical validation. For example, if an article documents a Kubernetes upgrade and includes Helm values, deprecated API fields, and commands tied to a specific cluster version, a reviewer can approve the draft without catching a mismatch that only shows up against the target version. Running the examples in a version-pinned environment before the draft is even created would catch a different class of errors. The draft gives you a checkpoint; reproducible validation gives you stronger evidence to review.

Collapse
 
jabinpou profile image
Mahdi JabinPour •

The draft-first step is a good boundary for this workflow. One failure case worth covering is a request that times out after the server has already created the draft: immediately retrying POST can duplicate it. I would check the dashboard before retrying an ambiguous result, and add a request timeout so a stalled call does not hang the script.

Collapse
 
emilywoodsnyc profile image
Emily Woods •

I'd add a quick check for link fragments when cross-posting. An image can load and the canonical URL can be correct, while a table-of-contents link still points to a heading ID from the original site. I'd open a couple of those jumps in the DEV draft before publishing, especially on longer posts. That is easy to miss when the Markdown itself looks fine.