Integrations Documentation
Set up your CMS connection. This guide covers WordPress plugin setup, webhooks, publishing, and troubleshooting.
Getting started
- Create a project from the Onboarding flow (or via Settings).
- Connect an integration (WordPress recommended).
- Configure publishing settings and schedule content.
WordPress plugin (recommended)
- Download the plugin: Distribb Plugin (.zip)
- In WP Admin → Plugins → Add New → Upload Plugin → select the .zip → Install → Activate.
- Open WP Admin → Distribb, copy your Integration Key.
- In Distribb → Integrations → WordPress → enter Site URL and Integration Key, then Connect.
Notes:
- Draft vs Publish behavior is controlled from the WP plugin settings.
- The plugin exposes secure endpoints under
/wp-json/distribb/v1/. - Syncing edits back to WordPress: after you edit a published article in Distribb, click Sync to WordPress on the article page. The plugin’s
/submitendpoint locates the existing post by slug and updates it in place — no duplicates are created. Keep the slug stable on the WordPress side for sync to work reliably.
Optional: style callouts to match your theme
Distribb articles include semantic classes for visual callouts so your WordPress theme controls the look. If your theme doesn’t style them, paste the snippet below into WP Admin → Appearance → Customize → Additional CSS to match the on-platform preview. Tweak colors to match your brand.
blockquote {
border-left: 4px solid #3b82f6;
margin: 1.5em 0;
padding: 1em 1.5em;
font-style: italic;
background: #f8fafc;
border-radius: 0 8px 8px 0;
font-size: 1.1em;
color: #1e293b;
}
.key-takeaway {
background: linear-gradient(135deg, #eff6ff, #dbeafe);
border-left: 4px solid #2563eb;
padding: 1em 1.5em;
margin: 1.5em 0;
border-radius: 0 8px 8px 0;
}
.key-takeaway strong { color: #1e40af; }
.stat-highlight {
text-align: center;
padding: 1.5em;
margin: 1.5em 0;
background: #f0fdf4;
border-radius: 12px;
border: 1px solid #bbf7d0;
}
.stat-highlight .stat-number {
display: block;
font-size: 2.5em;
font-weight: 800;
color: #16a34a;
line-height: 1.2;
}
.stat-highlight .stat-label {
display: block;
font-size: .95em;
color: #374151;
margin-top: .3em;
}
.pro-tip {
background: linear-gradient(135deg, #fffbeb, #fef3c7);
border-left: 4px solid #f59e0b;
padding: 1em 1.5em;
margin: 1.5em 0;
border-radius: 0 8px 8px 0;
}
.pro-tip strong { color: #92400e; }
WordPress (App Password fallback)
We now default to the plugin. If you still need App Password:
- Create an Application Password for your WP user (Users → Profile).
- In Distribb → Integrations → WordPress → enter Site URL, Username, and App Password.
This path remains supported for backward compatibility.
API Webhook
Use a custom webhook to receive article payloads directly from Distribb. This lets you connect any custom CMS, internal tool, or third-party platform that accepts HTTP POST requests.
Setup
- Go to Integrations → API Webhook.
- Enter your Webhook Endpoint — a publicly reachable HTTPS URL that accepts POST requests.
- Enter your Access Token — any secret string your server uses to authenticate incoming requests.
- Click Test Your Webhook to verify your endpoint returns a
2xxresponse before saving.
How it works
Distribb sends the full article as JSON to your endpoint with POST. Initial publication uses event_type: "publish_articles" and may retry up to three times on server errors or timeouts.
To send edited content, open the published article and click Send changes to Webhook, or ask the Distribb Agent to update it. Distribb saves your edits and sends event_type: "update_articles" to the same URL with the same access token. You can send changes even when your receiver did not return a public URL.
Store Distribb’s article id on first publish. On an update, use that ID to find and update the existing post. For older posts without a stored ID, match the existing slug once and save the ID. If no post matches, return 404; never create a post from an update event.
Request headers
POST {your-endpoint}
Content-Type: application/json
Authorization: Bearer {your-access-token}
Idempotency-Key: {unique-delivery-key}
User-Agent: Distribb-Publisher/1.0
Request body
{
"event_type": "publish_articles",
"data": {
"articles": [
{
"id": "63452",
"title": "10 Tips for Better SEO in 2025",
"slug": "10-tips-for-better-seo-2025",
"content_html": "<h2>Introduction</h2><p>HTML content...</p>",
"content_markdown": "## Introduction
Markdown content...",
"meta_description": "Improve your rankings with these 10 proven SEO tips.",
"image_url": "https://example.com/images/featured.jpg",
"alt_text": "A laptop with SEO charts on screen",
"tags": ["SEO", "Content Marketing", "Digital Marketing"],
"author": "Jane Smith",
"status": "Published",
"is_update": false
}
]
}
}
Field reference
| Field | Type | Description |
|---|---|---|
event_type (top level) | string | "publish_articles" on the first send for a slug; "update_articles" when the user clicks Sync to CMS on an article that is already live. Route on this to decide create vs update. |
id | string | Stable Distribb article ID. Store it as a unique post identifier. Later deliveries with the same article ID can contain new edits; use the Idempotency-Key header to deduplicate delivery attempts. |
title | string | The article title. |
slug | string | URL slug (e.g. my-article-title). Use as a fallback to match older posts that have no stored Distribb ID. Preserve the existing permalink when updating content. |
url, published_url | string, optional | The existing stored article URL, when known. An omitted URL does not prevent an update by ID. |
external_id | string, optional | The remote post ID, when known. |
created_at, published_at | string, optional | Original article dates in UTC. An update omits unknown dates; preserve your existing dates when a field is absent. |
updated_at | string | When the saved content was modified, or the delivery time when no modification date is available. |
content_html | string | Full article body as HTML. Use this if your platform renders HTML. |
content_markdown | string | Full article body as Markdown. Use this if your platform accepts Markdown. |
meta_description | string | SEO meta description for the article. |
image_url | string | null | Featured image URL. null if no image is set. |
alt_text | string | null | Alt text for the featured image. null if none. |
tags | array of strings | List of keywords / tags for the article. |
author | string | Display name of the article author. |
status | string | "Published" or "Draft". Sync preserves the article’s current publication status. |
is_update | boolean | true on sync events, false on first publish. Redundant with the top-level event_type but available if your router inspects the article object directly. |
update_only | boolean | true requires an existing post. Return an error if it cannot be found. |
Handling sync / update events
Add the update branch to your existing webhook route. This example uses placeholder database methods; map the fields to your CMS and keep your existing token validation. Make the article ID unique in your database so repeated publish deliveries cannot create duplicates.
// Inside your authenticated Node.js / Express receiver
app.post('/my-webhook', async (req, res) => {
const eventType = req.body?.event_type;
const articles = req.body?.data?.articles || [];
const results = [];
for (const article of articles) {
if (eventType === 'update_articles' || article.is_update || article.update_only) {
const existing = await db.posts.findOne({ distribb_id: article.id })
|| await db.posts.findOne({ slug: article.slug, distribb_id: null });
if (!existing) {
return res.status(404).json({ success: false, error: 'Article not found for update' });
}
await db.posts.update(existing.id, {
...mapArticleFields(article),
distribb_id: article.id,
slug: existing.slug,
created_at: existing.created_at,
published_at: article.published_at || existing.published_at
});
results.push({ id: article.id, action: 'updated', published_url: existing.url });
} else if (eventType === 'publish_articles') {
// Atomic insert-or-update by a unique Distribb ID for initial delivery retries.
const post = await db.posts.upsertByDistribbId(article.id, mapArticleFields(article));
results.push({ id: article.id, action: 'published', published_url: post.url });
} else {
return res.status(422).json({ success: false, error: 'Unsupported event type' });
}
}
res.status(200).json({ success: true, results });
});
Expected response
Return 200 after saving the update, with the existing public URL when you know it. Return 202 if you queued the work. A 2xx confirms receipt; Distribb does not treat that alone as proof that your public page has changed. Explicit errors, skipped updates, zero processed articles, or an unexpected create response are reported as failures.
HTTP/1.1 200 OK
Content-Type: application/json
{ "success": true, "action": "updated", "published_url": "https://example.com/blog/my-article" }
An update failure leaves the existing article published in Distribb and keeps its URL. Distribb does not automatically resend an update after a timeout or server error because your receiver may already have applied it. Check your receiver’s log or the public page before sending again.
Authentication
Your Access Token is sent as a Bearer token in every request:
Authorization: Bearer your-secret-token-here
Verify this header on your server and reject unrecognized tokens with 401 Unauthorized.
Example — Node.js / Express
const express = require('express');
const app = express();
app.use(express.json());
app.post('/my-webhook', (req, res) => {
const token = (req.headers['authorization'] || '').replace('Bearer ', '');
if (token !== process.env.MY_SECRET_TOKEN) {
return res.status(401).json({ error: 'Unauthorized' });
}
const articles = req.body?.data?.articles || [];
for (const article of articles) {
console.log('Received:', article.title, '| Status:', article.status);
// Save to your CMS or database here
}
res.status(200).json({ success: true });
});
app.listen(3000);
Example — Python / Flask
from flask import Flask, request, jsonify
import os
app = Flask(__name__)
@app.route('/my-webhook', methods=['POST'])
def receive_article():
auth = request.headers.get('Authorization', '')
if auth != f"Bearer {os.environ['MY_SECRET_TOKEN']}":
return jsonify({'error': 'Unauthorized'}), 401
articles = request.json.get('data', {}).get('articles', [])
for article in articles:
print(f"Received: {article['title']} ({article['status']})")
# Save to your DB or CMS here
return jsonify({'success': True}), 200
Testing
Use the Test Your Webhook button in the integration modal to send a sample payload to your endpoint before going live. This confirms your URL is reachable, your token is accepted, and your handler responds with a 2xx.
Troubleshooting
- WordPress 403/401: Verify Integration Key (plugin) or App Password (fallback).
- Plugin not detected: Ensure the plugin is activated and the Integration Key is saved.
- Draft URL: Draft posts show
?p=IDlinks until published—this is normal.
Security
- Token-based publishing (plugin) is preferred over Basic Auth.
- We recommend rotating plugin Integration Keys periodically.
- Hide system errors from users and show friendly messages.
Changelog (highlights)
- 1.0.0: Initial public plugin release; token-based publishing; docs page added.