Skip to main content
RapidDev - Software Development Agency
API AutomationsWordPressBasic Auth

How to Automate WordPress Blog Publishing using the API

Automate WordPress blog publishing by uploading a featured image with POST /wp-json/wp/v2/media (multipart), then creating the post with POST /wp-json/wp/v2/posts using HTTP Basic Auth with Application Passwords over HTTPS. The key constraint: creating with status 'publish' requires the Editor or Administrator role — Author-level Application Passwords can only create drafts. WordPress core has no rate limits, but managed hosts (WP Engine, Kinsta) enforce WAF throttles.

Need help automating? Talk to an expert
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner6 min read15-30 minutesWordPressMay 2026RapidDev Engineering Team
TL;DR

Automate WordPress blog publishing by uploading a featured image with POST /wp-json/wp/v2/media (multipart), then creating the post with POST /wp-json/wp/v2/posts using HTTP Basic Auth with Application Passwords over HTTPS. The key constraint: creating with status 'publish' requires the Editor or Administrator role — Author-level Application Passwords can only create drafts. WordPress core has no rate limits, but managed hosts (WP Engine, Kinsta) enforce WAF throttles.

API Quick Reference

Auth

HTTP Basic Auth (Application Passwords)

Rate limit

No core limit; host WAF throttles vary

Format

JSON

SDK

REST only

Understanding the WordPress REST API

The WordPress REST API (v2) exposes your WordPress content via JSON endpoints under /wp-json/wp/v2/. It has been stable since WordPress 4.7 and supports full CRUD for posts, pages, media, categories, tags, users, and custom post types registered with show_in_rest: true.

Authentication for automation uses Application Passwords — a core WordPress feature since version 5.6 (December 2020). They use HTTP Basic Auth over HTTPS with a format of username:application_password. Application Passwords are REST-only (cannot log into wp-admin), individually revocable, and do not bypass 2FA. They are the officially recommended authentication method for REST API integrations.

For blog publishing automation, the typical flow is: upload featured image, get the media ID, create post with featured_media set to that ID. Posts can be created as drafts or published directly. Official docs at developer.wordpress.org/rest-api/.

Base URLhttps://yourwordpresssite.com/wp-json

Setting Up WordPress API Authentication

WordPress Application Passwords are generated per-user in wp-admin. They are 24-character passwords displayed with spaces (e.g., 'abcd 1234 efgh 5678 ijkl 9012') — strip spaces before Base64 encoding for the Authorization header. HTTPS is required outside localhost. REST-only: you cannot use an Application Password to log into the dashboard.

  1. 1Log into your WordPress wp-admin dashboard
  2. 2Go to Users > All Users, click on the account you want to use for automation
  3. 3Scroll down to 'Application Passwords' section (requires WordPress 5.6+)
  4. 4Enter a name like 'Blog Automation' and click 'Add New Application Password'
  5. 5Copy the displayed password immediately — it is shown only once
  6. 6Your credentials are: WordPress username + the Application Password
  7. 7Test: curl -I 'https://yoursite.com/wp-json/wp/v2/posts' -H 'Authorization: Basic BASE64_HERE' — should return 200
auth.py
1import os
2import base64
3import requests
4
5WP_BASE = os.environ['WP_BASE_URL'] # e.g. https://yoursite.com/wp-json
6WP_USER = os.environ['WP_USERNAME']
7WP_PASS = os.environ['WP_APP_PASSWORD'] # Application Password (spaces stripped)
8
9# Create Basic Auth header
10credentials = f"{WP_USER}:{WP_PASS.replace(' ', '')}"
11token = base64.b64encode(credentials.encode()).decode()
12
13WP_HEADERS = {
14 'Authorization': f'Basic {token}',
15 'Content-Type': 'application/json'
16}
17
18# Verify auth
19resp = requests.get(f'{WP_BASE}/wp/v2/users/me', headers=WP_HEADERS)
20print('Authenticated as:', resp.json().get('name'))

Security notes

  • Never use Application Passwords over plain HTTP — HTTPS is mandatory outside localhost
  • Store the username and Application Password in environment variables, never in source code
  • Create a dedicated automation account (Editor role) rather than using your personal admin credentials
  • Application Passwords are individually revocable — create one per integration so you can revoke compromised ones without affecting others
  • Rotate Application Passwords every 90 days as a security best practice

Key endpoints

POST/wp/v2/media

Upload a media file (image, video, PDF) to the WordPress media library. Use multipart/form-data. Returns the media object including its ID, which you pass as featured_media when creating the post.

ParameterTypeRequiredDescription
filestringrequiredBinary file data sent as multipart/form-data. The field name is 'file'.
Content-DispositionstringrequiredHeader specifying filename: 'attachment; filename=image.jpg'. Required for WordPress to process the upload.
titlestringoptionalOptional title for the media item. Can be set in the form data.

Request

json
1multipart/form-data with file field containing the image binary and Content-Disposition header

Response

json
1{"id":123,"source_url":"https://yoursite.com/wp-content/uploads/2026/05/image.jpg","title":{"rendered":"image"},"media_type":"image","mime_type":"image/jpeg"}
POST/wp/v2/posts

Create a new blog post. Set status to 'publish' to publish immediately (requires publish_posts capability = Editor+), or 'draft' for draft. Include the media ID in featured_media.

ParameterTypeRequiredDescription
titlestringrequiredPost title as a plain string.
contentstringrequiredPost body as HTML. WordPress stores and renders it as HTML.
statusstringoptional'publish', 'draft', 'private', 'pending', 'future'. Default is 'draft'. 'publish' requires publish_posts capability.
categoriesarrayoptionalArray of category IDs. Fetch IDs first with GET /wp/v2/categories.
tagsarrayoptionalArray of tag IDs. Fetch with GET /wp/v2/tags.
featured_medianumberoptionalMedia ID from the POST /wp/v2/media upload response.
datestringoptionalISO 8601 date for scheduled publishing. Combined with status: 'future'.

Request

json
1{"title":"How to Deploy on Vercel","content":"<p>Step-by-step guide...</p>","status":"publish","categories":[5],"tags":[12,15],"featured_media":123}

Response

json
1{"id":456,"link":"https://yoursite.com/how-to-deploy-on-vercel/","title":{"rendered":"How to Deploy on Vercel"},"status":"publish","featured_media":123}
GET/wp/v2/categories

List available categories. Returns category IDs and names for use in the post creation payload.

ParameterTypeRequiredDescription
per_pagenumberoptionalResults per page, max 100. Default 10.
searchstringoptionalSearch categories by name.

Response

json
1[{"id":5,"name":"Tutorials","slug":"tutorials"},{"id":7,"name":"Development","slug":"development"}]
GET/wp/v2/tags

List available tags with their IDs for use when creating posts.

ParameterTypeRequiredDescription
per_pagenumberoptionalResults per page, max 100.
searchstringoptionalSearch tags by name.

Response

json
1[{"id":12,"name":"API","slug":"api"},{"id":15,"name":"Automation","slug":"automation"}]

Step-by-step automation

1

Upload Featured Image to WordPress Media Library

Why: Featured images must be uploaded separately before creating the post — WordPress needs the media ID to link the image.

Send POST /wp-json/wp/v2/media with the image as multipart/form-data. The Content-Disposition header must specify the filename. Do not set Content-Type to application/json for media uploads — use multipart/form-data. The response contains the media ID you'll use in the next step.

request.sh
1curl -X POST 'https://yoursite.com/wp-json/wp/v2/media' \
2 -H 'Authorization: Basic BASE64_USER_APPPASS' \
3 -H 'Content-Disposition: attachment; filename=featured-image.jpg' \
4 -H 'Content-Type: image/jpeg' \
5 --data-binary @'/path/to/featured-image.jpg'

Pro tip: Set the Content-Type header to the correct MIME type for your image (image/jpeg, image/png, image/webp). An incorrect MIME type can cause WordPress to reject the upload or misclassify the file.

Expected result: A media object with id, source_url, and metadata. Store the id value for use as featured_media in the post creation call.

2

Fetch Category and Tag IDs

Why: WordPress uses numeric IDs for categories and tags in the API — you cannot pass category names directly when creating a post.

Send GET /wp-json/wp/v2/categories and GET /wp-json/wp/v2/tags to list all available options. Build a name-to-ID lookup map so your automation can reference categories by name. You can also use the search parameter to find specific categories.

request.sh
1curl -X GET 'https://yoursite.com/wp-json/wp/v2/categories?per_page=100' \
2 -H 'Authorization: Basic BASE64_USER_APPPASS'
3
4curl -X GET 'https://yoursite.com/wp-json/wp/v2/tags?per_page=100' \
5 -H 'Authorization: Basic BASE64_USER_APPPASS'

Pro tip: Cache these maps — categories and tags change infrequently. Refetch only when a 400 error indicates an unknown ID, or refresh daily.

Expected result: Two name-to-ID maps you can use to resolve category/tag names from your content calendar to the numeric IDs required by the POST /wp/v2/posts endpoint.

3

Create the Blog Post

Why: With the media ID and category/tag IDs in hand, you can create a fully-formed published post in a single API call.

Send POST /wp-json/wp/v2/posts with the title, HTML content, status, categories, tags, and featured_media. The response includes the post URL in the 'link' field. Remember: status 'publish' requires the Editor or Administrator role — Author role creates draft only regardless of what status you request.

request.sh
1curl -X POST 'https://yoursite.com/wp-json/wp/v2/posts' \
2 -H 'Authorization: Basic BASE64_USER_APPPASS' \
3 -H 'Content-Type: application/json' \
4 -d '{
5 "title": "How to Automate Blog Publishing",
6 "content": "<h2>Introduction</h2><p>This guide covers...</p>",
7 "status": "publish",
8 "categories": [5],
9 "tags": [12, 15],
10 "featured_media": 123
11 }'

Pro tip: If you want to schedule the post for future publishing, set status to 'future' and provide a 'date' field in ISO 8601 format (e.g., '2026-06-01T09:00:00'). The post will go live at the specified time in the site's configured timezone.

Expected result: A post object with id, link (the public URL), status: 'publish', and all assigned taxonomy IDs. The post is immediately live on your site.

Complete working code

This complete script reads post data from a dictionary (simulating a content calendar entry), uploads the featured image, resolves category and tag IDs, creates the published post, and prints the live URL. Adapt it to read from Google Sheets, Notion, or Airtable as your content calendar source.

automate_wp_publishing.py
1import os
2import base64
3import requests
4
5WP_BASE = os.environ['WP_BASE_URL'] # e.g. https://yoursite.com/wp-json
6WP_USER = os.environ['WP_USERNAME']
7WP_PASS = os.environ['WP_APP_PASSWORD'].replace(' ', '')
8WP_AUTH = (WP_USER, WP_PASS)
9
10def get_taxonomy_ids(endpoint, names):
11 """Get IDs for given names, creating missing ones."""
12 resp = requests.get(f'{WP_BASE}/{endpoint}?per_page=100', auth=WP_AUTH)
13 resp.raise_for_status()
14 m = {item['name'].lower(): item['id'] for item in resp.json()}
15 ids = []
16 for name in names:
17 if name.lower() in m:
18 ids.append(m[name.lower()])
19 return ids
20
21def upload_image(image_path):
22 if not image_path or not os.path.exists(image_path):
23 return None
24 filename = os.path.basename(image_path)
25 ext = filename.rsplit('.', 1)[-1].lower()
26 mime_map = {'jpg':'image/jpeg','jpeg':'image/jpeg','png':'image/png','webp':'image/webp','gif':'image/gif'}
27 mime = mime_map.get(ext, 'image/jpeg')
28 with open(image_path, 'rb') as f:
29 resp = requests.post(
30 f'{WP_BASE}/wp/v2/media', auth=WP_AUTH,
31 headers={'Content-Disposition': f'attachment; filename={filename}', 'Content-Type': mime},
32 data=f.read()
33 )
34 resp.raise_for_status()
35 return resp.json()['id']
36
37def publish_post(post_data: dict) -> dict:
38 print(f"Publishing: {post_data['title']}")
39 media_id = upload_image(post_data.get('image_path'))
40 cat_ids = get_taxonomy_ids('wp/v2/categories', post_data.get('categories', []))
41 tag_ids = get_taxonomy_ids('wp/v2/tags', post_data.get('tags', []))
42 payload = {
43 'title': post_data['title'],
44 'content': post_data['content'],
45 'status': 'publish',
46 'categories': cat_ids,
47 'tags': tag_ids
48 }
49 if media_id:
50 payload['featured_media'] = media_id
51 resp = requests.post(f'{WP_BASE}/wp/v2/posts', auth=WP_AUTH,
52 headers={'Content-Type': 'application/json'}, json=payload)
53 resp.raise_for_status()
54 post = resp.json()
55 print(f'Published: {post["link"]}')
56 return post
57
58if __name__ == '__main__':
59 post = publish_post({
60 'title': 'Getting Started with WordPress REST API',
61 'content': '<h2>Introduction</h2><p>The WordPress REST API lets you automate publishing...</p>',
62 'categories': ['Tutorials', 'Development'],
63 'tags': ['API', 'WordPress', 'Automation'],
64 'image_path': '/tmp/featured-image.jpg'
65 })
66 print('Post ID:', post['id'])

Error handling

401{"code":"rest_not_logged_in","message":"You are not currently logged in.","data":{"status":401}}
Cause

The Authorization header is missing, malformed, or the Application Password is incorrect. Also triggers if Application Passwords are disabled by a plugin.

Fix

Verify the Base64 encoding: base64(username:application_password_without_spaces). Check that the Application Password was copied correctly (no extra spaces). Ensure no plugin has disabled Application Passwords.

Retry strategy

No retry — fix credentials.

403{"code":"rest_cannot_create","message":"Sorry, you are not allowed to create posts as this user.","data":{"status":403}}
Cause

The user account linked to the Application Password does not have the create_posts capability. This typically means the user is a Subscriber.

Fix

Use an Application Password from an Editor or Administrator account. Authors can create posts but only Editors and Admins can publish directly.

Retry strategy

No retry — fix the user role.

403{"code":"rest_forbidden","message":"Sorry, you are not allowed to publish posts.","data":{"status":403}}
Cause

The Application Password belongs to an Author-level user who can create posts but lacks the publish_posts capability.

Fix

Use an Editor or Administrator Application Password to publish directly. Alternatively, create the post as 'draft' (which Authors can do) and implement a separate publishing step with elevated credentials.

Retry strategy

No retry — role change required.

429HTTP 429 Too Many Requests (host-specific body, not standardized)
Cause

The managed WordPress host's WAF throttled your IP. WordPress core has no rate limits, but WP Engine, Kinsta, and others enforce per-IP limits that are not publicly documented.

Fix

Implement exponential backoff when receiving 429. Add 500ms delays between sequential posts. Contact your host for the specific rate limit policy.

Retry strategy

Exponential backoff: 2s, 4s, 8s, 16s. Check if the host returns a Retry-After header.

400{"code":"rest_invalid_param","message":"Invalid parameter(s): categories","data":{"status":400}}
Cause

One or more category IDs in the request do not exist in WordPress.

Fix

Fetch current categories with GET /wp/v2/categories before submitting. If a category does not exist, either create it first with POST /wp/v2/categories or remove it from the array.

Retry strategy

No retry — fix the category IDs.

Rate Limits for WordPress REST API

ScopeLimitWindow
WordPress coreNo built-in limitUnlimited by WordPress itself
WooCommerce Store API25 requestsper 10-second window (Checkout endpoint: 3 per 60 seconds)
Managed hosts (WP Engine, Kinsta, etc.)Undisclosed per-IP throttleHost-dependent; plan for 429 defensively
retry-handler.ts
1import time
2import requests
3
4def wp_request(method, url, auth, max_retries=5, **kwargs):
5 for attempt in range(max_retries):
6 resp = getattr(requests, method)(url, auth=auth, **kwargs)
7 if resp.status_code == 429:
8 retry_after = int(resp.headers.get('Retry-After', 2 ** attempt))
9 print(f'Throttled. Waiting {retry_after}s...')
10 time.sleep(retry_after)
11 continue
12 return resp
13 raise Exception(f'Max retries exceeded for {url}')
  • Add 500ms delays between sequential post creation calls to avoid managed host WAF throttles
  • Upload featured images in batch at the start of a publishing run, then create posts — separating phases makes retry logic simpler
  • Cache category and tag ID maps rather than fetching them before every post
  • Use WordPress's batch publishing by scheduling posts (status: future, date: ISO 8601) rather than publishing all at once
  • Test with status: 'draft' first to verify all IDs are correct before switching to status: 'publish'

Security checklist

  • Always use HTTPS — Application Passwords do not work over HTTP outside localhost
  • Store the WordPress username and Application Password in environment variables, never in source code
  • Create a dedicated automation account with Editor role rather than using your personal admin credentials
  • Create one Application Password per integration — revoke compromised passwords individually without affecting others
  • Rotate Application Passwords every 90 days
  • Sanitize HTML content before sending to WordPress to prevent XSS from untrusted content sources
  • Never expose the WP_APP_PASSWORD value in logs, error messages, or API responses

Automation use cases

Content Calendar to WordPress

beginner

Read scheduled posts from a Notion or Google Sheets content calendar and automatically publish them to WordPress on their scheduled dates.

AI-Generated Article Publisher

intermediate

Use an AI API to generate blog post content, then automatically upload to WordPress with SEO metadata, categories, and featured image from a stock photo API.

Multi-Site Blog Syndication

intermediate

Publish a single article to multiple WordPress sites (different WP_BASE_URL and Application Passwords) simultaneously for content distribution across a site network.

No-code alternatives

Don't want to write code? These platforms can automate the same workflows visually.

Zapier

Free tier available; paid plans from $19.99/month

Zapier has a WordPress integration that can create posts from Google Sheets rows, Notion database entries, or form submissions without writing any code.

Pros
  • + No code required
  • + Connects to 5,000+ content sources
  • + Handles scheduling and retries
Cons
  • - No featured image upload support in basic plans
  • - HTML content limited — strips some formatting
  • - Cost scales with post volume

Make

Free tier available; paid plans from $9/month

Make's WordPress module supports post creation with categories, tags, and featured image assignment from a media URL.

Pros
  • + Featured media support via HTTP module for image upload
  • + More affordable than Zapier at scale
  • + Better HTML handling
Cons
  • - More complex setup for image upload + post create multi-step flow
  • - Learning curve for non-technical users
  • - Webhook support for triggers on higher plans

n8n

Free self-hosted; cloud from €20/month

n8n's WordPress node supports creating posts with full metadata, and the HTTP Request node handles media uploads, making it a complete publishing automation.

Pros
  • + Self-hostable
  • + Full media upload support via HTTP Request node
  • + Code node for content transformation
Cons
  • - WordPress node may not support all REST fields
  • - Self-hosting requires server maintenance
  • - More setup than SaaS tools

Best practices

  • Upload the featured image before creating the post — you need the media ID first
  • Fetch and cache category and tag ID maps at script startup rather than per-post
  • Test with status: 'draft' before running in production with status: 'publish'
  • The X-WP-Total and X-WP-TotalPages response headers tell you how many items exist — use them for pagination rather than guessing
  • Use an Editor-level Application Password for publishing — Author accounts silently downgrade to 'draft' even if you request 'publish'
  • Send HTML content, not Markdown — WordPress stores and renders HTML natively
  • Add 500ms delays between posts when publishing in bulk to avoid managed host WAF throttles

Ask AI to help

Copy one of these prompts to get a personalized, working implementation.

ChatGPT / Claude Prompt

I'm automating WordPress blog publishing using the WordPress REST API with Application Passwords. I can create drafts successfully but when I set status: 'publish' I get a 403 rest_forbidden error. My Application Password belongs to a user with the 'Author' role. What role does a WordPress user need to publish posts via the REST API, and how do I check which role my automation account has?

Lovable / V0 Prompt

Build a Next.js content scheduling dashboard for WordPress blog publishing. The app should: display a table of scheduled blog posts from a Notion database (query via Notion API), allow clicking 'Publish Now' which calls a Server Action that uploads the featured image to WordPress via POST /wp-json/wp/v2/media, then creates the post via POST /wp-json/wp/v2/posts with the media ID, categories, and tags. Show the live WordPress post URL when done. Use WP Application Password auth (WP_USERNAME and WP_APP_PASSWORD from env vars, HTTPS required). Include a status column showing Draft, Scheduled, or Published for each post.

Frequently asked questions

Why does my post get created as a draft even though I set status: 'publish'?

This happens when the Application Password belongs to an Author-level user. Authors have create_posts capability but not publish_posts capability. WordPress silently downgrades the status to 'pending' or 'draft' instead of returning an error. Use an Application Password from an Editor or Administrator account to publish directly.

How do I create new categories or tags via the API?

Send POST /wp-json/wp/v2/categories with {"name": "My Category"} and POST /wp-json/wp/v2/tags with {"name": "My Tag"}. Both require the manage_categories capability (Editor+). The response includes the created ID to use in subsequent post creation calls.

Can I schedule a post for future publishing?

Yes. Set status to 'future' and include a 'date' field with an ISO 8601 datetime in the site's timezone: {"status": "future", "date": "2026-06-01T09:00:00"}. WordPress will publish it at that time automatically.

Is the WordPress REST API free?

The WordPress REST API is built into WordPress core and is free to use. Self-hosted WordPress has no API usage costs. WordPress.com (the hosted service) has its own REST API with rate limits that are not publicly documented, but Automattic does not charge per-API-call fees.

What happens when I hit the rate limit on a managed host?

Managed WordPress hosts (WP Engine, Kinsta, SiteGround) return HTTP 429 when their WAF throttles your requests. The exact limits are not published. Implement exponential backoff starting at 2 seconds and add 500ms delays between sequential publishing calls to avoid triggering the throttle.

How do I update an existing post instead of creating a new one?

Send POST /wp-json/wp/v2/posts/{id} to replace a post, or PATCH /wp-json/wp/v2/posts/{id} for a partial update. You need the post ID, which you can find by searching with GET /wp-json/wp/v2/posts?search=post+title&status=any.

Do Application Passwords work with 2FA enabled on my WordPress account?

Yes. Application Passwords bypass 2FA by design — they are REST API credentials only, separate from your login flow. 2FA protects your wp-admin login, but Application Passwords authenticate API calls directly without going through the 2FA challenge.

Can RapidDev build a custom WordPress publishing automation?

Yes. RapidDev has built 600+ apps including content pipeline automations that connect Notion, Google Sheets, and AI content generators to WordPress publishing workflows. We can build a fully managed solution tailored to your content calendar. Visit rapidevelopers.com for a free consultation.

RapidDev

Need this automated?

Our team has built 600+ apps with API automations. We can build this for you.

Book a free consultation

Skip the coding — we'll build it for you

Our experts have built 600+ API automations. From prototype to production in days, not weeks.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.