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
HTTP Basic Auth (Application Passwords)
No core limit; host WAF throttles vary
JSON
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/.
https://yourwordpresssite.com/wp-jsonSetting 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.
- 1Log into your WordPress wp-admin dashboard
- 2Go to Users > All Users, click on the account you want to use for automation
- 3Scroll down to 'Application Passwords' section (requires WordPress 5.6+)
- 4Enter a name like 'Blog Automation' and click 'Add New Application Password'
- 5Copy the displayed password immediately — it is shown only once
- 6Your credentials are: WordPress username + the Application Password
- 7Test: curl -I 'https://yoursite.com/wp-json/wp/v2/posts' -H 'Authorization: Basic BASE64_HERE' — should return 200
1import os2import base643import requests45WP_BASE = os.environ['WP_BASE_URL'] # e.g. https://yoursite.com/wp-json6WP_USER = os.environ['WP_USERNAME']7WP_PASS = os.environ['WP_APP_PASSWORD'] # Application Password (spaces stripped)89# Create Basic Auth header10credentials = f"{WP_USER}:{WP_PASS.replace(' ', '')}"11token = base64.b64encode(credentials.encode()).decode()1213WP_HEADERS = {14 'Authorization': f'Basic {token}',15 'Content-Type': 'application/json'16}1718# Verify auth19resp = 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
/wp/v2/mediaUpload 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
file | string | required | Binary file data sent as multipart/form-data. The field name is 'file'. |
Content-Disposition | string | required | Header specifying filename: 'attachment; filename=image.jpg'. Required for WordPress to process the upload. |
title | string | optional | Optional title for the media item. Can be set in the form data. |
Request
1multipart/form-data with file field containing the image binary and Content-Disposition headerResponse
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"}/wp/v2/postsCreate 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
title | string | required | Post title as a plain string. |
content | string | required | Post body as HTML. WordPress stores and renders it as HTML. |
status | string | optional | 'publish', 'draft', 'private', 'pending', 'future'. Default is 'draft'. 'publish' requires publish_posts capability. |
categories | array | optional | Array of category IDs. Fetch IDs first with GET /wp/v2/categories. |
tags | array | optional | Array of tag IDs. Fetch with GET /wp/v2/tags. |
featured_media | number | optional | Media ID from the POST /wp/v2/media upload response. |
date | string | optional | ISO 8601 date for scheduled publishing. Combined with status: 'future'. |
Request
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
1{"id":456,"link":"https://yoursite.com/how-to-deploy-on-vercel/","title":{"rendered":"How to Deploy on Vercel"},"status":"publish","featured_media":123}/wp/v2/categoriesList available categories. Returns category IDs and names for use in the post creation payload.
| Parameter | Type | Required | Description |
|---|---|---|---|
per_page | number | optional | Results per page, max 100. Default 10. |
search | string | optional | Search categories by name. |
Response
1[{"id":5,"name":"Tutorials","slug":"tutorials"},{"id":7,"name":"Development","slug":"development"}]/wp/v2/tagsList available tags with their IDs for use when creating posts.
| Parameter | Type | Required | Description |
|---|---|---|---|
per_page | number | optional | Results per page, max 100. |
search | string | optional | Search tags by name. |
Response
1[{"id":12,"name":"API","slug":"api"},{"id":15,"name":"Automation","slug":"automation"}]Step-by-step automation
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.
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.
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.
1curl -X GET 'https://yoursite.com/wp-json/wp/v2/categories?per_page=100' \2 -H 'Authorization: Basic BASE64_USER_APPPASS'34curl -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.
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.
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": 12311 }'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.
1import os2import base643import requests45WP_BASE = os.environ['WP_BASE_URL'] # e.g. https://yoursite.com/wp-json6WP_USER = os.environ['WP_USERNAME']7WP_PASS = os.environ['WP_APP_PASSWORD'].replace(' ', '')8WP_AUTH = (WP_USER, WP_PASS)910def 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 ids2021def upload_image(image_path):22 if not image_path or not os.path.exists(image_path):23 return None24 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']3637def 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_ids48 }49 if media_id:50 payload['featured_media'] = media_id51 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 post5758if __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
{"code":"rest_not_logged_in","message":"You are not currently logged in.","data":{"status":401}}The Authorization header is missing, malformed, or the Application Password is incorrect. Also triggers if Application Passwords are disabled by a plugin.
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.
No retry — fix credentials.
{"code":"rest_cannot_create","message":"Sorry, you are not allowed to create posts as this user.","data":{"status":403}}The user account linked to the Application Password does not have the create_posts capability. This typically means the user is a Subscriber.
Use an Application Password from an Editor or Administrator account. Authors can create posts but only Editors and Admins can publish directly.
No retry — fix the user role.
{"code":"rest_forbidden","message":"Sorry, you are not allowed to publish posts.","data":{"status":403}}The Application Password belongs to an Author-level user who can create posts but lacks the publish_posts capability.
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.
No retry — role change required.
HTTP 429 Too Many Requests (host-specific body, not standardized)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.
Implement exponential backoff when receiving 429. Add 500ms delays between sequential posts. Contact your host for the specific rate limit policy.
Exponential backoff: 2s, 4s, 8s, 16s. Check if the host returns a Retry-After header.
{"code":"rest_invalid_param","message":"Invalid parameter(s): categories","data":{"status":400}}One or more category IDs in the request do not exist in WordPress.
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.
No retry — fix the category IDs.
Rate Limits for WordPress REST API
| Scope | Limit | Window |
|---|---|---|
| WordPress core | No built-in limit | Unlimited by WordPress itself |
| WooCommerce Store API | 25 requests | per 10-second window (Checkout endpoint: 3 per 60 seconds) |
| Managed hosts (WP Engine, Kinsta, etc.) | Undisclosed per-IP throttle | Host-dependent; plan for 429 defensively |
1import time2import requests34def 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 continue12 return resp13 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
beginnerRead scheduled posts from a Notion or Google Sheets content calendar and automatically publish them to WordPress on their scheduled dates.
AI-Generated Article Publisher
intermediateUse 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
intermediatePublish 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/monthZapier has a WordPress integration that can create posts from Google Sheets rows, Notion database entries, or form submissions without writing any code.
- + No code required
- + Connects to 5,000+ content sources
- + Handles scheduling and retries
- - 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/monthMake's WordPress module supports post creation with categories, tags, and featured image assignment from a media URL.
- + Featured media support via HTTP module for image upload
- + More affordable than Zapier at scale
- + Better HTML handling
- - 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/monthn8n's WordPress node supports creating posts with full metadata, and the HTTP Request node handles media uploads, making it a complete publishing automation.
- + Self-hostable
- + Full media upload support via HTTP Request node
- + Code node for content transformation
- - 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.
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?
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.
Need this automated?
Our team has built 600+ apps with API automations. We can build this for you.
Book a free consultation