Skip to Content
ProvidersWordPress

WordPress

Category: Publishing / CMS
Integration type: Tenant-configured (REST API or WP CLI; stored in integrations)
External API: WordPress REST API v2 or WP CLI via SSH


Purpose

WordPress is the primary blog publishing destination. After the Blog Writer agent generates content and HITL approval is granted, the Blog Publisher worker pushes the post to the tenant’s WordPress site. The integration supports:

  • Creating draft posts (always — never auto-publish)
  • Uploading featured images
  • Setting categories and tags
  • Attaching author metadata
  • Checking post status after upload

The platform always creates posts as draft — publication is the DM agency’s responsibility. Auto-publish is intentionally not supported.


Config Structure

Tenants can connect WordPress in two ways:

Available on any WordPress site (hosted or self-hosted). Requires an Application Password (WP 5.6+):

interface WordPressRestConfig { siteUrl: string; // e.g. "https://acmeplumbing.com" username: string; // WordPress username appPassword: string; // Application Password (generated in WP → Users → Profile → Application Passwords) }

Option B: WP CLI over SSH (advanced)

For tenants where REST API is disabled or firewalled:

interface WordPressSshConfig { host: string; // SSH host port: number; // SSH port (default 22) username: string; // SSH username privateKey: string; // RSA private key (PEM format) wpPath: string; // Path to WordPress installation, e.g. "/var/www/html" }

Both config types are stored in integrations.api_key (encrypted) and differentiated by metadata.connectionType: 'rest' | 'ssh'.


Integration Pattern

REST API connection (packages/tools/src/wordpress.ts)

class WordPressRestClient { private auth: string; constructor(private cfg: WordPressRestConfig) { this.auth = Buffer.from(`${cfg.username}:${cfg.appPassword}`).toString('base64'); } async createDraftPost(post: { title: string; content: string; // HTML excerpt?: string; categories?: number[]; tags?: number[]; authorId?: number; featuredImageId?: number; slug?: string; }): Promise<{ postId: number; editUrl: string }> { const response = await axios.post( `${this.cfg.siteUrl}/wp-json/wp/v2/posts`, { title: post.title, content: post.content, excerpt: post.excerpt, status: 'draft', // Always draft categories: post.categories ?? [], tags: post.tags ?? [], author: post.authorId, featured_media: post.featuredImageId, slug: post.slug, }, { headers: { Authorization: `Basic ${this.auth}`, 'Content-Type': 'application/json', }, }, ); return { postId: response.data.id, editUrl: `${this.cfg.siteUrl}/wp-admin/post.php?post=${response.data.id}&action=edit`, }; } async uploadImage(imageBuffer: Buffer, filename: string, mimeType: string): Promise<{ mediaId: number; url: string }> { const response = await axios.post( `${this.cfg.siteUrl}/wp-json/wp/v2/media`, imageBuffer, { headers: { Authorization: `Basic ${this.auth}`, 'Content-Type': mimeType, 'Content-Disposition': `attachment; filename="${filename}"`, }, }, ); return { mediaId: response.data.id, url: response.data.source_url, }; } async getCategories(): Promise<{ id: number; name: string; slug: string }[]> { const response = await axios.get( `${this.cfg.siteUrl}/wp-json/wp/v2/categories?per_page=100`, { headers: { Authorization: `Basic ${this.auth}` } }, ); return response.data.map((c: any) => ({ id: c.id, name: c.name, slug: c.slug })); } async verify(): Promise<void> { // Test credentials by fetching current user info const response = await axios.get( `${this.cfg.siteUrl}/wp-json/wp/v2/users/me`, { headers: { Authorization: `Basic ${this.auth}` } }, ); if (!response.data.id) throw new Error('WordPress verification failed'); } }

Blog Publisher workflow

Blog Writer agent completes → BlogPost stored in MongoDB ▼ (HITL approval granted) Blog Publisher worker picks up ├── Fetch BlogPost content from MongoDB ├── Fetch featured image from S3 (if any) ├── Upload image to WP Media Library ├── Resolve category IDs from tenant's WP site ├── Create draft post via REST API └── Store postId + editUrl in blog_posts.external_id + blog_posts.publish_url

Test Cases

Unit tests (packages/tools/src/wordpress.test.ts)

TestApproach
createDraftPost() always sets status: 'draft'Mock axios.post; assert status === 'draft'
createDraftPost() sends correct auth headerAssert Authorization: Basic ...
createDraftPost() returns postId and editUrlMock response { id: 42 }; assert editUrl contains 42
uploadImage() sends image as binary bodyMock axios.post; assert Content-Type and Content-Disposition headers
verify() calls /users/me endpointAssert URL and auth header
verify() throws when id absentMock {}; assert error thrown
Throws on 401 (invalid app password)Mock 401; assert error
Throws on 404 (WP REST API disabled)Mock 404; assert descriptive error

Integration tests

TestApproach
Create draft on WordPress.com test siteUse test site; assert post created in drafts
Upload image and use as featured mediaUpload 1×1 PNG; create post with featuredImageId; assert media attached
Verify with valid credentialsAssert user info returned

Local WP dev environment

Use Local (by Flywheel) or Docker WP for local integration testing:

# docker-compose.dev.yml wordpress: image: wordpress:latest ports: - "8080:80" environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: wp WORDPRESS_DB_PASSWORD: wp WORDPRESS_DB_NAME: wp depends_on: - db

© 2026 Leadmetrics — Internal use only