Publish WordPress Drafts with Custom GPT + OpenAPI


ChatGPT Image Sep 10, 2025, 12_35_56 AM
Public blog post to WordPress using a Custom GPT

It’s been quite a while since I wrote my very first article on this WordPress blog, and I remember using tools to make editing and publishing easier. I recall one called ‘Write’ (if I’m not mistaken). Today, many years later, and as you can imagine, I now rely on AI to help me with editing and publishing — the tool we were all looking for back then 😀

If you work with AI assistants to produce technical content, connecting a Custom GPT directly to your WordPress.com site allows you to create draft posts automatically — no copy-paste, no friction.

This guide walks you through:

  • ✅ Creating a WordPress.com developer app
  • ✅ Getting ApiKey and/or OAuth2 tokens
  • ✅ Using a clean OpenAPI 3.1.0 spec in your GPT Actions
  • ✅ Uploading media and creating drafts
  • ✅ Configuring your Custom GPT description and instructions
  • ✅ Troubleshooting & security best practices

🛠️ Prerequisites

RequirementDetails
WordPress.com accountEditor/Author/Admin role with publishing rights
Sitee.g., elguerre.com
Toolscurl (Linux/macOS/WSL) or PowerShell equivalent
SecurityAbility to manage Application Passwords and developer apps

🆕 Step 1: Create a WordPress.com Developer App

Go to developer.wordpress.com/apps and create an app. Save your client_id and client_secret.

🔑 Step 2: Generate an Application Password (for development)

Create an Application Password at wordpress.com/me/security. Copy the 24-character password and store it securely.

🔐 Use Application Passwords for non-interactive clients. Works even with 2FA enabled.

🎟️ Step 3: Get an Access Token

curl -X POST "https://public-api.wordpress.com/oauth2/token" \
  -d "client_id=<CLIENT_ID>" \
  -d "client_secret=<CLIENT_SECRET>" \
  -d "grant_type=password" \
  -d "username=<YOUR_EMAIL>" \
  -d "password=<APPLICATION_PASSWORD>"

🏷️ Step 4: Verify Token and Get Site ID

# Validate token
curl "https://public-api.wordpress.com/oauth2/token-info?client_id=<CLIENT_ID>&token=<ACCESS_TOKEN>"

# List sites and IDs
curl -H "Authorization: Bearer <ACCESS_TOKEN>" \
  "https://public-api.wordpress.com/rest/v1.1/me/sites"

Use either the numeric site_id or the domain (e.g., elguerre.com) in API calls.

📄 Step 5: Paste the OpenAPI 3.1.0

Before we configure the Custom GPT itself, we need to provide it with the right schema. Copy the following OpenAPI 3.1.0 specification into the Actions section of the GPT Builder. This schema exposes the WordPress.com endpoints (posts, media, categories, tags) that your GPT will later use to create and manage drafts.

{
  "openapi": "3.1.0",
  "info": {
    "title": "WordPress.com Publishing API",
    "version": "1.0.2"
  },
  "servers": [
    {
      "url": "https://public-api.wordpress.com"
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      }
    },
    "schemas": {
      "PostCreate": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "content": {
            "type": "string",
            "description": "HTML or Markdown"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "publish",
              "future",
              "pending",
              "private"
            ]
          },
          "slug": {
            "type": "string"
          },
          "excerpt": {
            "type": "string"
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "integer"
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "integer"
            }
          },
          "featured_media": {
            "type": "integer"
          }
        },
        "required": [
          "title",
          "content",
          "status"
        ]
      },
      "MediaUpload": {
        "type": "object",
        "properties": {
          "file": {
            "type": "string",
            "format": "binary"
          },
          "title": {
            "type": "string"
          },
          "caption": {
            "type": "string"
          },
          "alt_text": {
            "type": "string"
          }
        },
        "required": [
          "file"
        ]
      }
    }
  },
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/wp/v2/sites/{site}/posts": {
      "post": {
        "operationId": "createPost",
        "summary": "Create a post (draft by default)",
        "parameters": [
          {
            "name": "site",
            "in": "path",
            "required": true,
            "description": "Your site domain or site ID, e.g. elguerre.com OR numeric site id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PostCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created"
          },
          "400": {
            "description": "Bad Request"
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/wp/v2/sites/{site}/media": {
      "post": {
        "operationId": "uploadMedia",
        "summary": "Upload media and return media id",
        "parameters": [
          {
            "name": "site",
            "in": "path",
            "required": true,
            "description": "Your site domain or site ID, e.g. elguerre.com OR numeric site id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/MediaUpload"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created"
          },
          "400": {
            "description": "Bad Request"
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/wp/v2/sites/{site}/categories": {
      "get": {
        "operationId": "listCategories",
        "summary": "List categories",
        "parameters": [
          {
            "name": "site",
            "in": "path",
            "required": true,
            "description": "Your site domain or site ID, e.g. elguerre.com OR numeric site id",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 100
            }
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    },
    "/wp/v2/sites/{site}/tags": {
      "get": {
        "operationId": "listTags",
        "summary": "List tags",
        "parameters": [
          {
            "name": "site",
            "in": "path",
            "required": true,
            "description": "Your site domain or site ID, e.g. elguerre.com OR numeric site id",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 100
            }
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    }
  }
}

✅ Step 6: Smoke Tests

Run these tests in the GPT Builder or with cURL:

  • List categories
  • Upload media
  • Create a draft post

⚙️ Configuring Your Custom GPT

Once the Actions are defined, you still need to configure the GPT correctly so that it behaves as your WordPress Tech Publisher:

  • Description: Example: “You are TechBlog GPT – .NET & Cloud Writer. You draft SEO-ready technical articles about .NET, C#, ASP.NET Core, Azure, Kubernetes, Docker, DevOps, and Angular. You can publish drafts to WordPress.com using configured Actions.”
  • prompt for your use case — then copy and refine that into the Description field here.
  • Conversation starters: Provide simple entry points like:
    • “Draft a tutorial about deploying .NET Aspire apps on AKS.”
    • “Generate a blog post about GitHub Actions CI/CD for ASP.NET Core.”
    • “Help me prepare a comparison between Dapr and gRPC for .NET microservices.”
  • Instructions: Include workflow principles (accuracy, reproducibility, SEO, guardrails). Be explicit about using status: draft when creating posts. Of course, to get the best results from your GPT, you’ll need a well-crafted prompt. Think of the description as the foundation of your assistant’s personality and workflow. If you’re not sure how to phrase it, you can even ask ChatGPT itself to propose an optimized
  • Actions: Paste the full OpenAPI spec, configure Bearer token auth, and test endpoints.
  • Testing: Run a first conversation to generate a short test post, push it as Draft, and confirm it appears in WordPress.com dashboard.

💻 cURL toolbox

curl -X POST -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -F "media=@cover.png" \
  "https://public-api.wordpress.com/wp/v2/sites/<SITE>/media"
curl -X POST -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Custom GPT — .NET Tech Blog Writer & WordPress Publisher (WordPress-ready)",
    "content": "<p>Draft content…</p>",
    "status": "draft",
    "slug": "custom-gpt-dotnet-tech-blog-writer-wordpress-ready",
    "excerpt": "AI-assisted publishing to WordPress.com."
  }' \
  "https://public-api.wordpress.com/wp/v2/sites/<SITE>/posts"

🐞 Troubleshooting

ErrorCauseFix
401 UnauthorizedExpired or invalid tokenRefresh or regenerate token
400 Bad RequestMalformed JSONEnsure required fields
redirect_uri mismatchWrong URL in OAuth appUpdate app redirect URI

🔐 Security Best Practices

  • ✅ Always revoke unused tokens
  • ✅ Store secrets in a password manager or secret vault
  • ✅ Use least-privilege WordPress.com roles (Author/Editor)
  • ✅ For production: prefer Authorization Code flow with refresh tokens

🎁 Bonus: Strengthening Your GPT Instructions

When defining the Instructions of your Custom GPT, it’s very useful to include an additional guideline that ensures your drafts are always published in the correct WordPress Gutenberg block format.

This way, your GPT will automatically generate posts that:

  • Preserve the structure (headings, lists, code, tables, quotes).
  • Open links in a new tab securely.
  • Avoid the common problem of being published in Classic editor mode.

👉 Copy and paste the following block directly into the Instructions of your GPT Builder:

## When publishing content through WordPress.com Actions, always follow these rules:

Use raw mode for content  
Send the post body (content) in raw mode.  
This avoids truncation and ensures full fidelity of the article.  

Use Gutenberg blocks, not Classic editor  
Wrap every section in proper Gutenberg block syntax (<!-- wp:... -->).  

Example for a heading:  
<!-- wp:heading -->
<h2>Step 1: Create a WordPress.com Developer App</h2>
<!-- /wp:heading -->

Code blocks  
Always wrap code in <!-- wp:code --> blocks:  

<!-- wp:code -->
<pre class="wp-block-code"><code>curl -X POST "https://..."</code></pre>
<!-- /wp:code -->

Lists, tables, quotes  
Use specific Gutenberg blocks:  
- Lists → <!-- wp:list --> ... <!-- /wp:list -->  
- Tables → <!-- wp:table --> ... <!-- /wp:table -->  
- Quotes → <!-- wp:quote --> ... <!-- /wp:quote -->  

Links  
For all external links, add:  

target="_blank" rel="noopener noreferrer"  

Example:  
<a href="https://developer.wordpress.com/apps/" target="_blank" rel="noopener noreferrer">WordPress.com Developer Apps</a>

📝 Conclusion

With this setup, your Custom GPT can securely publish draft posts into WordPress.com. You now have:

  • A working OAuth2 flow with Application Passwords
  • A clean OpenAPI spec for GPT Actions
  • Verified media upload and draft creation
  • A configured GPT with description, conversation starters, and proper guardrails

🚀 Next: run your GPT workflow, generate a tech article, and let the Action push it as a draft.

📚 References

PS: ⚠️ This article was drafted with the support of my custom GPT assistant. I’ve been fine-tuning and configuring it along the way, so this post is also part of that journey.

Un comentario sobre “Publish WordPress Drafts with Custom GPT + OpenAPI

Deja un comentario

Este sitio utiliza Akismet para reducir el spam. Conoce cómo se procesan los datos de tus comentarios.