DocsAPI Integration Guide

Build Your Own Blog with FluxPoster Integration

Learn how to create a self-hosted blog that automatically receives content from FluxPoster, including HTML text, inline images, videos, and thumbnails using open web standards.

Overview

FluxPoster uses open web standards to publish content to your blog, ensuring maximum compatibility and future-proofing. This guide will walk you through setting up your own blog to receive automatic content updates, including:

Rich HTML Content

Full HTML articles with proper formatting

Inline Images

Embedded images within your content

Video Embeds

YouTube, Vimeo, and custom video players

Featured Images

Thumbnails and cover images for posts

⚡ Quick Start

The fastest way to get started is using our Micropub endpoint. It's a W3C standard that works with dozens of existing blogging clients.

Jump to Micropub setup

Supported Open Standards

FluxPoster supports multiple open standards for maximum compatibility with your existing infrastructure. Choose the one that best fits your setup:

StandardBest ForFeatures
Micropub (W3C)RecommendedModern IndieWeb sitesFull CRUD, media uploads, syndication
REST APICustom implementationsFull control, JSON payloads
XML-RPC (MetaWeblog)WordPress, BloggerLegacy support, wide compatibility
WebhooksEvent-driven systemsPush notifications, flexible
RSS/JSON FeedPull-based systemsSimple, universal support
AtomPubAtom-based systemsStandards compliant, RESTful

Micropub API (Recommended)

Micropubis a W3C recommendation for creating, updating, and deleting posts on websites. It's the modern standard for the IndieWeb and works with many existing tools.

1. Implement the Micropub Endpoint

Create an endpoint at /micropub on your server:

micropub-endpoint.js (Node.js/Express)
const express = require('express');
const app = express();

// Micropub endpoint
app.post('/micropub', express.json(), async (req, res) => {
  // Verify the access token
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!verifyToken(token)) {
    return res.status(401).json({ error: 'unauthorized' });
  }

  const { type, properties } = req.body;

  // Handle h-entry (blog post)
  if (type?.[0] === 'h-entry') {
    const post = {
      title: properties.name?.[0] || 'Untitled',
      content: properties.content?.[0]?.html || properties.content?.[0],
      excerpt: properties.summary?.[0],
      tags: properties.category || [],
      featuredImage: properties.photo?.[0],
      published: properties['post-status']?.[0] !== 'draft',
    };

    // Save to your database
    const savedPost = await savePost(post);
    
    // Return 201 with Location header
    res.status(201)
       .header('Location', `https://yourblog.com/posts/${savedPost.slug}`)
       .end();
  }
});

// Configuration endpoint
app.get('/micropub', (req, res) => {
  if (req.query.q === 'config') {
    return res.json({
      'media-endpoint': 'https://yourblog.com/micropub/media',
      'post-types': [
        { type: 'article', name: 'Article' },
        { type: 'note', name: 'Note' },
      ],
    });
  }
  res.status(400).json({ error: 'Unknown query' });
});

app.listen(3000);

2. Add Media Endpoint for Images

To receive inline images and thumbnails, implement the media endpoint:

media-endpoint.js
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });

app.post('/micropub/media', upload.single('file'), async (req, res) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!verifyToken(token)) {
    return res.status(401).json({ error: 'unauthorized' });
  }

  // Process the uploaded file
  const file = req.file;
  
  // Move to permanent storage (S3, local, etc.)
  const publicUrl = await uploadToStorage(file);
  
  // Return the URL in the Location header
  res.status(201)
     .header('Location', publicUrl)
     .end();
});

3. Configure FluxPoster

Add your Micropub endpoint as a destination in FluxPoster:

  1. 1Go to Brands → [Your Brand] → Destinations
  2. 2Click Add Destination and select Custom Blog
  3. 3Enter your Micropub endpoint URL
  4. 4Add your access token for authentication

REST API Integration

For custom implementations, you can use FluxPoster's REST API or implement your own REST endpoint to receive content.

Expected Payload Structure

FluxPoster sends a CanonicalPost object:

Canonical Post JSON Schema
{
  "id": "uuid-string",
  "title": "Your Blog Post Title",
  "slug": "your-blog-post-title",
  "bodyHtml": "<h2>Introduction</h2><p>Your content...</p><img src=\"https://cdn.example.com/image.jpg\" alt=\"Description\">",
  "excerpt": "A brief summary of the post",
  
  "images": [
    {
      "id": "img-1",
      "cdnUrl": "https://cdn.fluxposter.com/uploads/featured.jpg",
      "alt": "Featured image description",
      "role": "featured",
      "width": 1200,
      "height": 630,
      "mimeType": "image/jpeg"
    },
    {
      "id": "img-2",
      "cdnUrl": "https://cdn.fluxposter.com/uploads/inline-1.jpg",
      "alt": "Inline image",
      "role": "inline"
    }
  ],
  "featuredImageId": "img-1",
  
  "tags": ["technology", "ai", "automation"],
  "categories": ["Blog"],
  
  "metaTitle": "SEO-optimized title",
  "metaDescription": "SEO meta description for search engines",
  
  "status": "published",
  "publishedAt": "2024-01-15T10:00:00Z",
  
  "authorName": "Your Brand",
  "sourceId": "draft-uuid",
  "sourceBrandId": "brand-uuid"
}

Implement Your REST Endpoint

rest-endpoint.py (Python/Flask)
from flask import Flask, request, jsonify
import uuid

app = Flask(__name__)

@app.route('/api/posts', methods=['POST'])
def create_post():
    # Verify authentication
    token = request.headers.get('Authorization', '').replace('Bearer ', '')
    if not verify_token(token):
        return jsonify({'error': 'Unauthorized'}), 401
    
    data = request.json
    
    # Extract post data
    post = {
        'id': str(uuid.uuid4()),
        'title': data['title'],
        'slug': data.get('slug') or slugify(data['title']),
        'content': data['bodyHtml'],
        'excerpt': data.get('excerpt'),
        'tags': data.get('tags', []),
        'featured_image': None,
        'published': data['status'] == 'published',
    }
    
    # Handle images
    for image in data.get('images', []):
        if image['role'] == 'featured':
            post['featured_image'] = image['cdnUrl']
        # Inline images are already in bodyHtml as <img> tags
    
    # Save to database
    saved_post = save_to_database(post)
    
    return jsonify({
        'success': True,
        'postId': saved_post['id'],
        'postUrl': f"https://yourblog.com/posts/{saved_post['slug']}"
    }), 201

if __name__ == '__main__':
    app.run(port=5000)

Webhook Integration

Webhooks allow FluxPoster to push content to your system in real-time. This is ideal for event-driven architectures or when you need to trigger additional workflows.

Webhook Events

EventTrigger
post.createdNew post is published
post.updatedExisting post is modified
post.deletedPost is removed
media.uploadedNew image/video uploaded
Webhook Payload Example
{
  "event": "post.created",
  "timestamp": "2024-01-15T10:00:00Z",
  "signature": "sha256=abc123...",
  "data": {
    "post": {
      "id": "post-uuid",
      "title": "New Blog Post",
      "bodyHtml": "<p>Content...</p>",
      "images": [...],
      "status": "published"
    },
    "brand": {
      "id": "brand-uuid",
      "name": "Your Brand"
    }
  }
}

Verify Webhook Signatures

Always verify the webhook signature to ensure requests are from FluxPoster:

verify-signature.js
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(`sha256=${expected}`)
  );
}

// In your webhook handler
app.post('/webhook', express.json(), (req, res) => {
  const signature = req.headers['x-fluxposter-signature'];
  
  if (!verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  // Process the webhook...
  const { event, data } = req.body;
  
  switch (event) {
    case 'post.created':
      handleNewPost(data.post);
      break;
    case 'post.updated':
      handlePostUpdate(data.post);
      break;
    // ...
  }
  
  res.status(200).json({ received: true });
});

Media Handling

FluxPoster supports multiple ways to handle images and videos in your content.

Image Delivery Options

CDN URLs

Use our CDN URLs directly in your HTML. Images are already optimized and served globally.

Easiest

Self-Hosted

Download images from our CDN and host them yourself for full control.

Full Control

Platform Upload

We upload images directly to your platform's media library (WordPress, Ghost, etc.).

Native

Processing Inline Images

If you want to download and self-host images, here's how to process the HTML:

process-images.js
const cheerio = require('cheerio');
const fetch = require('node-fetch');
const fs = require('fs');
const path = require('path');

async function processPostImages(post) {
  const $ = cheerio.load(post.bodyHtml);
  const imagePromises = [];
  
  // Find all images in the HTML
  $('img').each((i, img) => {
    const src = $(img).attr('src');
    if (src && src.includes('cdn.fluxposter.com')) {
      imagePromises.push(
        downloadAndReplace($, img, src)
      );
    }
  });
  
  await Promise.all(imagePromises);
  
  // Also handle featured image
  if (post.featuredImageId) {
    const featuredImage = post.images.find(
      img => img.id === post.featuredImageId
    );
    if (featuredImage) {
      post.localFeaturedImage = await downloadImage(
        featuredImage.cdnUrl,
        'featured-' + path.basename(featuredImage.cdnUrl)
      );
    }
  }
  
  post.bodyHtml = $.html();
  return post;
}

async function downloadAndReplace($, imgElement, originalUrl) {
  const filename = path.basename(originalUrl);
  const localPath = await downloadImage(originalUrl, filename);
  $(imgElement).attr('src', '/uploads/' + filename);
}

async function downloadImage(url, filename) {
  const response = await fetch(url);
  const buffer = await response.buffer();
  const localPath = path.join('public/uploads', filename);
  fs.writeFileSync(localPath, buffer);
  return localPath;
}

Video Embeds

Videos are embedded as standard <iframe> tags:

Video Embed HTML
<!-- YouTube Embed -->
<div class="video-embed" data-provider="youtube">
  <iframe 
    src="https://www.youtube.com/embed/VIDEO_ID"
    width="560" 
    height="315"
    frameborder="0"
    allowfullscreen>
  </iframe>
</div>

<!-- Vimeo Embed -->
<div class="video-embed" data-provider="vimeo">
  <iframe 
    src="https://player.vimeo.com/video/VIDEO_ID"
    width="560" 
    height="315"
    frameborder="0"
    allowfullscreen>
  </iframe>
</div>

Video thumbnails are included in the images array with role: "thumbnail".

Authentication

Secure your endpoints to ensure only FluxPoster can publish to your blog.

Bearer Token Authentication

The simplest method—FluxPoster sends a token in the Authorization header:

Request Headers
POST /micropub HTTP/1.1
Host: yourblog.com
Authorization: Bearer your-secret-token
Content-Type: application/json

IndieAuth (OAuth for IndieWeb)

For a more robust solution, implementIndieAuth, which provides OAuth-style authentication:

IndieAuth Flow
1. FluxPoster discovers your auth endpoint via <link rel="authorization_endpoint">
2. User authorizes FluxPoster at your authorization endpoint
3. FluxPoster exchanges code for access token at your token endpoint
4. FluxPoster uses token for all subsequent requests

Generate a Secure Token

Generate Token (Node.js)
const crypto = require('crypto');

// Generate a secure random token
const token = crypto.randomBytes(32).toString('hex');
console.log('Your API token:', token);

// Store this in your environment variables
// FLUXPOSTER_API_TOKEN=your-generated-token

Complete Examples

Here are complete, runnable examples for popular frameworks:

Next.js (App Router)

app/api/micropub/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_KEY!
);

export async function POST(request: NextRequest) {
  // Verify token
  const token = request.headers.get('authorization')?.replace('Bearer ', '');
  if (token !== process.env.MICROPUB_TOKEN) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const body = await request.json();
  const properties = body.properties || {};

  // Create the post
  const { data: post, error } = await supabase
    .from('posts')
    .insert({
      title: properties.name?.[0] || 'Untitled',
      content: properties.content?.[0]?.html || properties.content?.[0],
      excerpt: properties.summary?.[0],
      featured_image: properties.photo?.[0],
      tags: properties.category || [],
      published: properties['post-status']?.[0] !== 'draft',
      created_at: new Date().toISOString(),
    })
    .select()
    .single();

  if (error) {
    return NextResponse.json({ error: error.message }, { status: 500 });
  }

  return new NextResponse(null, {
    status: 201,
    headers: {
      Location: `${process.env.NEXT_PUBLIC_URL}/posts/${post.id}`,
    },
  });
}

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  
  if (searchParams.get('q') === 'config') {
    return NextResponse.json({
      'media-endpoint': `${process.env.NEXT_PUBLIC_URL}/api/micropub/media`,
      'post-types': [{ type: 'article', name: 'Article' }],
    });
  }

  return NextResponse.json({ error: 'Unknown query' }, { status: 400 });
}

Express.js (Complete Server)

server.js
const express = require('express');
const multer = require('multer');
const cors = require('cors');
const { v4: uuidv4 } = require('uuid');

const app = express();
app.use(cors());
app.use(express.json());

const upload = multer({ dest: 'uploads/' });
const posts = new Map(); // In production, use a real database

// Middleware to verify token
const authenticate = (req, res, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (token !== process.env.API_TOKEN) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  next();
};

// Micropub endpoint
app.post('/micropub', authenticate, (req, res) => {
  const { type, properties } = req.body;
  
  const post = {
    id: uuidv4(),
    title: properties?.name?.[0] || 'Untitled',
    content: properties?.content?.[0]?.html || properties?.content?.[0] || '',
    excerpt: properties?.summary?.[0] || '',
    featuredImage: properties?.photo?.[0],
    tags: properties?.category || [],
    published: properties?.['post-status']?.[0] !== 'draft',
    createdAt: new Date().toISOString(),
  };
  
  posts.set(post.id, post);
  
  res.status(201)
     .header('Location', `http://localhost:3000/posts/${post.id}`)
     .end();
});

// Micropub config
app.get('/micropub', (req, res) => {
  if (req.query.q === 'config') {
    return res.json({
      'media-endpoint': 'http://localhost:3000/micropub/media',
      'post-types': [{ type: 'article', name: 'Article' }],
    });
  }
  res.status(400).json({ error: 'Unknown query' });
});

// Media upload endpoint
app.post('/micropub/media', authenticate, upload.single('file'), (req, res) => {
  const file = req.file;
  if (!file) {
    return res.status(400).json({ error: 'No file uploaded' });
  }
  
  const publicUrl = `http://localhost:3000/uploads/${file.filename}`;
  
  res.status(201)
     .header('Location', publicUrl)
     .end();
});

// Serve uploaded files
app.use('/uploads', express.static('uploads'));

// View posts
app.get('/posts/:id', (req, res) => {
  const post = posts.get(req.params.id);
  if (!post) {
    return res.status(404).json({ error: 'Post not found' });
  }
  res.json(post);
});

app.listen(3000, () => {
  console.log('Blog server running on http://localhost:3000');
});

More Examples

Find complete examples in our GitHub repository, including implementations for Django, Rails, Laravel, Go, and more.

RSS & JSON Feeds

If you prefer a pull-based approach, FluxPoster provides standard RSS and JSON feeds that your blog can consume:

RSS 2.0 Feed

/blog/feed.xml

JSON Feed

/blog/feed.json

Set up a cron job to periodically fetch new content from these feeds and import into your blog. Both feeds include full HTML content, featured images, and metadata.