Platforms
Connect a WordPress.com or self-hosted WordPress site to publish and schedule articles, run its WooCommerce store and install ad pixels.
A connected WordPress site is a blog account. You use it to create, schedule, update and delete the site's posts through the Blogs API. A self-hosted site that runs WooCommerce is also a store, so the same account works with the Commerce API. WordPress accounts are never targets of POST /v1/posts and have no inbox or social analytics.
#What you can do
| Job | WordPress.com | Self-hosted |
|---|---|---|
| Publish, schedule, edit and delete articles | Yes | Yes |
| Featured images, tags and authors | Yes | Yes |
| WooCommerce products, stock, coupons and orders | No | Yes, when WooCommerce is installed |
| WordPress pages (Commerce pages endpoints) | Yes | Yes |
| Ad pixels as theme widgets | Plans with plugin support only | Yes |
| Property | Value |
|---|---|
| Platform id | wordpress (store objects report platform: "woocommerce") |
| Connect | WordPress.com: OAuth. Self-hosted: username plus an Application Password |
| Blogs per account | One. Self-hosted sites use blog id "1"; WordPress.com uses the site's numeric id |
| Scheduling | Native: WordPress publishes at publishDate |
| Article body | Raw HTML in bodyHtml |
#Before you start
The WordPress user who connects must be able to publish posts and upload files. Pozy checks both before it saves the account, so a contributor role that can only write drafts is refused. Each account is one site. To manage several sites, connect each one.
#Connect WordPress.com
curl "https://api.pozy.ai/v1/connect/wordpress?profileId=6710c3a9e4b0d2f1a8c95e21&redirect_url=https://app.example.com/connected" \
-H "Authorization: Bearer $POZY_API_KEY"const res = await fetch("https://api.pozy.ai/v1/connect/wordpress?profileId=6710c3a9e4b0d2f1a8c95e21&redirect_url=https:%2F%2Fapp.example.com%2Fconnected", {
headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
// Redirect the site owner to data.authUrlimport os, requests
resp = requests.get(
"https://api.pozy.ai/v1/connect/wordpress",
headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
params={"profileId": "6710c3a9e4b0d2f1a8c95e21", "redirect_url": "https://app.example.com/connected"},
)
auth_url = resp.json()["authUrl"]The site owner approves access and picks one site on WordPress.com. Pozy asks for single-site access only, never access to every site on the WordPress.com account. Reconnecting the same site on the same profile refreshes the account in place.
When the connection fails, the browser returns to your redirect_url with error and platform=wordpress:
error |
Meaning |
|---|---|
wordpress_access_denied |
The user declined |
wordpress_code_rejected |
The authorization expired or was already used. Start again |
wordpress_no_site |
No site was picked |
wordpress_insufficient_permissions |
The user is not an Author, Editor or Administrator of the site |
wordpress_site_unavailable, wordpress_unavailable |
The site or WordPress.com did not answer. Try again in a few minutes |
wordpress_connection_failed |
Anything else. Start again |
#Connect a self-hosted site
The site owner opens Users > Profile in WordPress admin, creates an Application Password, and gives it to you with their login username.
curl -X POST https://api.pozy.ai/v1/connect/wordpress/token \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"profileId": "6710c3a9e4b0d2f1a8c95e21",
"siteUrl": "https://blog.example.com",
"username": "editor@example.com",
"applicationPassword": "abcd efgh ijkl mnop qrst uvwx"
}'const res = await fetch("https://api.pozy.ai/v1/connect/wordpress/token", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
profileId: "6710c3a9e4b0d2f1a8c95e21",
siteUrl: "https://blog.example.com",
username: "editor@example.com",
applicationPassword: "abcd efgh ijkl mnop qrst uvwx",
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.account._id);resp = requests.post(
"https://api.pozy.ai/v1/connect/wordpress/token",
headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
json={
"profileId": "6710c3a9e4b0d2f1a8c95e21",
"siteUrl": "https://blog.example.com",
"username": "editor@example.com",
"applicationPassword": "abcd efgh ijkl mnop qrst uvwx",
},
)
account_id = resp.json()["account"]["_id"]siteUrl must be the public HTTPS root of the site. Leave out /wp-json, paths and any credentials. Before saving, Pozy finds the site's REST API, refuses private or local network addresses, checks the credentials and confirms the user can publish and upload.
Store them like any other credential. Revoking the password in WordPress admin cuts access at once. Reconnect with a new password to restore it; the account keeps its id.
#Publish an article
List the blog first and use blogs[0].id. That way the same code works for self-hosted ("1") and WordPress.com (numeric site id) accounts.
curl -X POST https://api.pozy.ai/v1/accounts/6710c4b2e4b0d2f1a8c95e3a/blogs/1/articles \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Five things we learned shipping our mobile app",
"bodyHtml": "<p>We spent six months on it. Here is what surprised us.</p>",
"excerpt": "Lessons from six months of mobile work.",
"tags": ["engineering", "mobile"],
"author": "7",
"image": { "url": "https://cdn.example.com/mobile-launch.jpg", "altText": "Phone showing the app home screen" },
"publishDate": "2026-11-12T08:00:00Z"
}'What WordPress does with each field:
| Field | WordPress behaviour |
|---|---|
isPublished |
New articles default to draft. true publishes now. false always wins, even with a future date, so leave it out when you schedule |
publishDate |
A future date schedules the post natively (status: "future"). The site's own cron publishes it |
tags |
Tag names, up to 100. Existing tags match by name, case-insensitive. Missing tags are created if the user may create terms |
author |
The numeric WordPress user id as a string, not a login or email |
image |
Downloaded into the media library and set as the featured image. Public JPEG, PNG, WebP or GIF up to 10 MB |
seo |
Rejected with 400. Core WordPress has no portable SEO fields |
Responses include WordPress's own status (publish, future, draft, pending or private). isPublished is true only for publish. On update, leaving out image keeps the current featured image, and there is no way to remove one through the API.
#WooCommerce
When a self-hosted site runs WooCommerce and the connected user can manage it, GET /v1/commerce/store?accountId=... lists store capabilities and the account works with the Commerce API. WordPress.com sites cannot reach WooCommerce through Pozy.
| Area | On WooCommerce |
|---|---|
| Not available | Sales channels, menus, redirects, metaobjects, markets, price lists, marketing activities, discount codes, reordering collection products (400 platform_not_supported) |
| Products | vendor, productType and seo are rejected. deactivate sets the product to draft, archive sets it to private |
| Collections | Product categories, with no SEO fields or sort order |
| Discounts | Coupons only. A coupon cannot start in the future or require a minimum quantity |
| Stock | One location with the id default. adjust reads then writes, so a sale in between can be overwritten |
| Metafields | Product custom fields only |
| Orders | Read-only, up to 100 per page, no extra permission needed |
| Optional permissions | None. grantPermissionsUrl is always null |
Pozy registers order and product webhooks on the site when you connect it, so you get commerce.order.* and commerce.product.* events. Order attribution needs WooCommerce 8.5 or later with Order Attribution turned on, and records only the last visit.
#Limits
- No social posts, comments, DMs, analytics or inbox on a WordPress account.
- You cannot create, rename or delete the site's blog (
405). - Custom post types and categories are not managed through the Blogs API. Pages are available through the Commerce pages endpoints.
- Deletes are permanent. Pozy keeps no copy. WordPress keeps the post's media and tags for other content.
#Errors
| Error | Cause | Fix |
|---|---|---|
400 on self-hosted connect |
siteUrl is not HTTPS, contains credentials, points at a private address or has no usable REST API |
Pass the public HTTPS site root |
401 or 403 on connect |
Wrong or revoked Application Password, or the user cannot publish and upload | Create a new password for a user with publish and upload rights |
400 on an article |
Invalid field, non-numeric author, or seo was sent |
Fix the field named in param |
404 blog_not_found |
Wrong blogId |
Call List blogs and use the id it returns |
422 tracking_tag_install_blocked |
The site cannot run pixel scripts. details.reason says why |
See Site pixels |
502, 503 |
The site failed or timed out | Check the article state before retrying a write; a timed-out write may have landed |
#Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/connect/wordpress |
Get WordPress.com OAuth connect URL |
POST /v1/connect/wordpress/token |
Connect self-hosted WordPress with an application password |
GET /v1/accounts/{accountId}/blogs |
List blogs |
GET /v1/accounts/{accountId}/blogs/{blogId} |
Get a blog |
GET /v1/accounts/{accountId}/blogs/{blogId}/articles |
List blog articles |
POST /v1/accounts/{accountId}/blogs/{blogId}/articles |
Create a blog article |
GET /v1/accounts/{accountId}/blogs/{blogId}/articles/{articleId} |
Get a blog article |
PATCH /v1/accounts/{accountId}/blogs/{blogId}/articles/{articleId} |
Update a blog article |
DELETE /v1/accounts/{accountId}/blogs/{blogId}/articles/{articleId} |
Delete a blog article |
#Webhook events
| Event | When it fires |
|---|---|
account.disconnected |
Account disconnected. |
commerce.product.created |
Commerce product created. |
commerce.product.updated |
Commerce product updated. |
commerce.product.deleted |
Commerce product deleted. |
commerce.order.created |
Commerce order created. |
commerce.order.updated |
Commerce order updated. |
commerce.order.refunded |
Commerce order refunded. |