Open app

Connect Platforms

Set up all publishing integrations including WordPress, Ghost, Webflow, Shopify, and custom Webhook endpoints.

theStacc supports 6 publishing integrations for Content SEO. Connect one or more to automatically publish blog content to your website. Manage all integrations under Content SEO > Settings > Publishing.

WordPress.com#

Hosted WordPress sites. Recommended for WordPress.com users.

  1. Click Connect WordPress.com
  2. Authorize via secure OAuth connection
  3. Sign in and grant theStacc access
  4. Select your target site if you have multiple

Each published blog includes title, body content (HTML), featured image, categories, tags, SEO meta data (Yoast/RankMath compatible), and post slug.

WordPress.org#

Self-hosted WordPress sites. Connect with an application password.

  1. In your WordPress admin, go to Users > Profile > Application Passwords
  2. Create a new password with the name "theStacc"
  3. Copy the generated password
  4. In theStacc, enter your site URL and the application password
  5. Click Test Connection to verify

Requires WordPress 5.6+ with REST API enabled (on by default). HTTPS recommended.

Custom Webhook#

Send blog posts to any platform via HTTP. Works with any system that accepts HTTP POST requests.

Building the receiver? See the Webhook Reference for the full developer guide — handshake, signing, every event, every field, response contract, database schemas, and ready-to-paste receiver examples in 6 languages.

  1. Enter your endpoint URL
  2. Add custom headers (optional) — for authentication tokens, API keys, or routing
  3. Click Test Connection to verify the URL is reachable
  4. Click Sample Payload to send the real publish-shape JSON and validate your parser

Two events your receiver must handle#

theStacc sends a different payload shape for each button. Branch on the event field — your receiver should handle both, or it will fail Test Connection even when the publish path works.

event: "test.ping" — fired by Test Connection. Liveness probe only. No blog fields.

{
  "event": "test.ping",
  "message": "This is a test from theStacc",
  "timestamp": "2026-04-30T12:00:00Z"
}

Respond 200 {"ok": true}. Do not require title, slug, or content for this event — there are none.

event: "blog.published" — fired on every real publish (and by Sample Payload with a preview- prefixed blog_id for receiver validation).

{
  "event": "blog.published",
  "blog_id": "8f3e...",
  "title": "10 SEO mistakes to avoid in 2026",
  "slug": "10-seo-mistakes-to-avoid-in-2026",
  "content": "<h2>Introduction</h2><p>...</p>",
  "excerpt": "Short description...",
  "excerpt_short": "Short description... (≤256 chars, word-boundary trimmed)",
  "meta_title": "10 SEO mistakes to avoid in 2026",
  "meta_description": "...",
  "featured_image_url": "https://cdn.thestacc.com/blogs/...jpg",
  "categories": ["SEO"],
  "tags": ["seo", "2026"],
  "keyword": "seo mistakes 2026",
  "published_at": "2026-04-30T12:00:00Z"
}

If blog_id starts with preview-, it's a sample call — accept it but skip your CMS write so test runs don't pollute your database.

What to return so the live URL appears in theStacc#

Respond with 200 (or 201) and a JSON body containing the live post URL. theStacc reads the response and stores both fields against the blog so the dashboard shows a clickable View live post link.

{
  "ok": true,
  "url": "https://your-cms.com/blog/10-seo-mistakes-to-avoid-in-2026",
  "id": "internal-cms-post-id"
}
  • url (or published_url) — the public URL of the blog on your site. If omitted, theStacc shows a *"Sent to webhook — your receiver didn't return a public URL"* warning.
  • id — your internal CMS post id. Stored as external_post_id so future updates / unpublishes can target the right record.

For preview calls (blog_id starts with preview-), it's fine to skip both fields and return {"ok": true, "skipped": true}.

Minimal receiver example (Next.js / Vercel)#

export async function POST(request) {
  const body = await request.json();

  if (body.event === "test.ping") {
    return Response.json({ ok: true });
  }

  if (body.event === "blog.published") {
    if (body.blog_id?.startsWith("preview-")) {
      return Response.json({ ok: true, skipped: true });
    }
    const post = await cms.posts.create({
      title: body.title,
      slug: body.slug,
      content: body.content,
      // ...
    });
    return Response.json({
      ok: true,
      url: `https://your-cms.com/blog/${post.slug}`,
      id: post.id,
    });
  }

  return Response.json({ error: "Unknown event" }, { status: 400 });
}

When you configure a webhook secret, theStacc signs every request with HMAC-SHA256 and sends the hex digest in the X-Webhook-Signature header. Verify it on every request — without verification, anyone who guesses your endpoint URL can post fake blogs to your CMS.

Critical: the signature is computed over the raw request body that theStacc sent, which is JSON serialized in compact form (json.dumps(payload, separators=(',', ':')) — no whitespace between keys and values). If you re-serialize the parsed JSON before hashing, the byte-for-byte representation will differ and the hashes won't match. Always hash the raw bytes you receive over the wire.

Node.js / Next.js:

import crypto from 'crypto';

export async function POST(request) {
  const rawBody = await request.text();           // raw bytes — do NOT parse first
  const sigHeader = request.headers.get('x-webhook-signature') || '';
  const expected = crypto
    .createHmac('sha256', process.env.STACC_WEBHOOK_SECRET)
    .update(rawBody)
    .digest('hex');

  // Length-check before timingSafeEqual — Node throws RangeError on
  // mismatched buffer lengths (e.g. when a probe sends a short or
  // empty X-Webhook-Signature). Crashing the receiver on every
  // garbage probe is a worse failure mode than 401-rejecting.
  const sigBuf = Buffer.from(sigHeader, 'utf8');
  const expBuf = Buffer.from(expected, 'utf8');
  if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
    return new Response('Invalid signature', { status: 401 });
  }

  const body = JSON.parse(rawBody);
  // ... handle event
}

Python / FastAPI:

import hmac, hashlib, os
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()
SECRET = os.environ["STACC_WEBHOOK_SECRET"].encode()

@app.post("/stacc-webhook")
async def receive(request: Request):
    raw = await request.body()
    sig = request.headers.get("x-webhook-signature", "")
    expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, sig):
        raise HTTPException(401, "Invalid signature")
    body = await request.json()
    # ... handle event

Python / Flask:

import hmac, hashlib, os
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["STACC_WEBHOOK_SECRET"].encode()

@app.post("/stacc-webhook")
def receive():
    raw = request.get_data()
    sig = request.headers.get("X-Webhook-Signature", "")
    expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, sig):
        abort(401)
    body = request.get_json()
    # ... handle event

PHP:

<?php
$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $raw, getenv('STACC_WEBHOOK_SECRET'));
if (!hash_equals($expected, $sig)) {
    http_response_code(401);
    exit('Invalid signature');
}
$body = json_decode($raw, true);
// ... handle event

If no webhook secret is configured, the X-Webhook-Signature header is omitted and your receiver must trust the URL alone — fine for development, not recommended in production.

All event types#

theStacc emits five different events on the same webhook URL. Branch on event.

EventWhen firedPayload
test.pingTest Connection button{event, message, timestamp}
blog.publishedFirst successful publishfull blog payload (see above)
blog.updatedRe-publish of an existing blogfull blog payload — same shape as blog.published
blog.unpublishedUser unpublishes from theStacc{event, blog_id, title}
blog.deletedUser deletes a published blog{event, blog_id, title}

For blog.updated / blog.unpublished / blog.deleted, look up the post in your CMS using the external_post_id you returned from blog.published (theStacc stores it and sends blog_id so you can map back).

Operational details#

  • Timeout: theStacc waits up to 15 seconds for a 2xx response. Slow CMS writes (image uploads, search indexing) will time out — return 2xx fast and do heavy work async.
  • No automatic retries. A 5xx or timeout fails the publish in theStacc and the user sees a "Failed to publish" toast. If your endpoint is occasionally slow, queue the actual CMS write internally and return 2xx immediately.
  • Redirects rejected. A 3xx response is treated as failure (anti-SSRF guardrail; a public webhook receiver that 302s to an internal address would otherwise bypass URL safety checks).
  • HTTPS only. http:// URLs and internal/private IP ranges are rejected at save time.
  • Idempotency. Use blog_id as a dedupe key. If theStacc ever resends (manual user retry), you'll see the same blog_id.
  • Test before writing receiver code. Point your webhook at webhook.site or requestbin.com first to inspect the actual request body and headers, then build your receiver against the real shape.

Best practices#

  • Use HTTPS — theStacc rejects http:// and internal addresses.
  • Return a 2xx status code. Redirects (3xx) are rejected.
  • Verify the X-Webhook-Signature header on every request when a secret is set.
  • Idempotency — use blog_id as a dedupe key in case of retries.
  • Set up error alerting on your endpoint so silent 4xx/5xx responses don't go unnoticed.

Ghost#

Publish directly to your Ghost blog via the Ghost Admin API.

  1. Click Connect Ghost
  2. Enter your Ghost site URL
  3. Enter your Ghost Admin API key (found in Ghost Admin > Settings > Integrations)
  4. Test the connection

Posts are created with full content, featured images, tags, and meta data.

Webflow#

Publish blog content to your Webflow CMS collections via a Webflow API token.

Before you start#

  • Your Webflow workspace must be on a plan that includes the CMS API (Workspace plans with CMS hosting). Free workspaces can read but cannot create CMS items.
  • The Webflow site must have been published at least once from the Designer. Webflow rejects live CMS publishes if the site has never been published.
  • A CMS Collection for blog posts must exist on the site. If you don't have one, create it in Webflow Designer → CMS → New Collection (recommended fields below).

Step 1 — Generate a Webflow API token#

  1. In Webflow, open the site you want to connect.
  2. Go to Site Settings → Apps & Integrations → API Access. You'll see this:
Webflow Site Settings → Apps & Integrations → API access section
Webflow Site Settings → Apps & Integrations → API access section
  1. Click the blue "Generate API token" button on the right — NOT "Generate V1 token". V1 tokens use Webflow's legacy API which theStacc doesn't support.
  2. On the next screen, name the token theStacc and set permissions exactly like this — Webflow shows a dropdown for each scope. The two scopes that need access are CMS and Sites; everything else stays on None.
ScopePermissionUnderlying API scopeWhy
CMSRead and writecms:read + cms:writeCreate and update blog items in your collection
SitesRead and writesites:read + sites:writeRead site info (for the live URL) and re-publish the site so new posts appear live
Custom codeNoneNot used
PagesNoneNot used
UsersNoneNot used
FormsNoneNot used
ComponentsNoneNot used
EcommerceNoneNot used
Authorized userRead onlyauthorized_user:readDefault — leave as-is

Webflow's UI labels (e.g. "Read and publish") are friendlier names for the underlying scopes. The token Stacc actually receives carries the API scopes shown in the right column. Source: Webflow Data API scopes reference.

  1. Click Generate token and copy it immediately — Webflow won't show it again.

Step 2 — Connect in theStacc#

  1. In theStacc, open Content SEO → Settings → Publishing and toggle Webflow on.
  2. Paste the token, give the integration a friendly name, and click Continue.
  3. Select your Webflow site from the dropdown.
  4. Select the CMS collection where blog posts should land.
  5. Map theStacc fields to your Webflow collection fields:

- Required: Title, Content, Slug

- Optional: Featured Image, Meta Description, Excerpt, Tags, Author, Category, Published Date, Is Featured

- Auto-mapping fills most of these based on field-name matches — review and adjust.

  1. Choose a publishing behavior:

- Publish Immediately — items go live the moment theStacc publishes them; theStacc also re-publishes the site so they appear on the live URL.

- Save as Draft — items appear in Webflow as drafts; you publish manually.

- Stage for Review — items are created and ready to publish; you press Publish in Webflow Designer.

  1. Click Create Integration.
FieldWebflow type
NamePlain text (auto-created — title goes here)
SlugURL slug (auto-created)
Post ContentRich text
Featured ImageImage
ExcerptPlain text
Meta TitlePlain text
Meta DescriptionPlain text
TagsPlain text (comma-separated)
AuthorPlain text

Webflow blog templates often use Reference / Multi-reference fields for Categories, Tags, and Authors. theStacc cannot yet auto-create the referenced items, so use Plain text fields for these for now.

Verify#

After saving the integration, click Test Connection. A green check confirms the token works and the collection is reachable.

Troubleshooting#

  • "Invalid API token" — token wrong, expired, or revoked. Generate a new one and update the integration.
  • "No CMS collections found" — the site has no CMS collections. Create one in Designer first.
  • "Item with this slug already exists" — a post with that slug was already published. Edit the slug or delete the existing item in Webflow.
  • Post published but URL returns 404 — most often the site has never been published from the Designer, or you're using a custom domain whose DNS hasn't propagated. Open Webflow Designer and click Publish at least once.
  • "409 Site not published" on first publish — Webflow rejects live CMS publishes on un-published sites. theStacc automatically falls back to staged item creation. Publish your site once in Designer; future live publishes work directly.
  • 403 Forbidden when creating items — the workspace plan doesn't include CMS API access. Upgrade to a Workspace plan with CMS hosting.

What gets sent to Webflow#

theStacc sends POST https://api.webflow.com/v2/collections/{id}/items/live (or /items for staged) with your mapped fields plus the required name and slug. No source code, design changes, or custom integrations are needed on the Webflow side.

Shopify#

Publish blog posts to your Shopify store's built-in blog.

  1. Click Connect Shopify
  2. Authorize theStacc to access your Shopify store
  3. Select the blog to publish to (if you have multiple)
  4. Posts are published as Shopify blog articles with content, images, and tags

Managing connections#

Each connected integration shows:

  • Status - Active, inactive, or disconnected
  • Last sync - When the last successful publish happened
  • Test button - Verify the connection works
  • Active/Inactive toggle - Pause publishing without disconnecting
  • Disconnect - Remove the integration entirely

Troubleshooting#

  • Connection failed - Verify your site URL includes https:// and the API is accessible
  • Authentication error - Regenerate your application password or API key
  • Publishing failed - Check user permissions (Author or Editor role minimum)
  • Images not uploading - Verify your CMS media library has available storage