Integrations Documentation

Set up your CMS connection. This guide covers WordPress plugin setup, webhooks, publishing, and troubleshooting.

Getting started

  1. Create a project from the Onboarding flow (or via Settings).
  2. Connect an integration (WordPress recommended).
  3. Configure publishing settings and schedule content.

WordPress plugin (recommended)

  1. Download the plugin: Distribb Plugin (.zip)
  2. In WP Admin → Plugins → Add New → Upload Plugin → select the .zip → Install → Activate.
  3. Open WP Admin → Distribb, copy your Integration Key.
  4. 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 /submit endpoint 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:

  1. Create an Application Password for your WP user (Users → Profile).
  2. 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

  1. Go to Integrations → API Webhook.
  2. Enter your Webhook Endpoint — a publicly reachable HTTPS URL that accepts POST requests.
  3. Enter your Access Token — any secret string your server uses to authenticate incoming requests.
  4. Click Test Your Webhook to verify your endpoint returns a 2xx response 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.
idstringStable 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.
titlestringThe article title.
slugstringURL 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_urlstring, optionalThe existing stored article URL, when known. An omitted URL does not prevent an update by ID.
external_idstring, optionalThe remote post ID, when known.
created_at, published_atstring, optionalOriginal article dates in UTC. An update omits unknown dates; preserve your existing dates when a field is absent.
updated_atstringWhen the saved content was modified, or the delivery time when no modification date is available.
content_htmlstringFull article body as HTML. Use this if your platform renders HTML.
content_markdownstringFull article body as Markdown. Use this if your platform accepts Markdown.
meta_descriptionstringSEO meta description for the article.
image_urlstring | nullFeatured image URL. null if no image is set.
alt_textstring | nullAlt text for the featured image. null if none.
tagsarray of stringsList of keywords / tags for the article.
authorstringDisplay name of the article author.
statusstring"Published" or "Draft". Sync preserves the article’s current publication status.
is_updatebooleantrue 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_onlybooleantrue 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=ID links 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.