Connect Platforms
Set up every Content SEO publishing destination — WordPress.com, WordPress.org, Custom Webhook, Ghost, Webflow, Shopify, and Zepio — including credential rules, status fields, and the errors you might see.
theStacc publishes your generated blog posts straight to your website. Connect one of the supported destinations and, depending on your publishing mode, new posts go live (or land as drafts) automatically. Manage every connection under Content SEO > Settings > Publishing, in the Destination section.
Supported destinations:
- WordPress.com (hosted WordPress)
- WordPress.org (self-hosted WordPress)
- Custom Webhook (any system that accepts an HTTP POST)
- Ghost
- Webflow
- Shopify (Beta)
- Zepio
Two rules that apply to every destination#
Only one destination is active at a time. A project publishes to a single place. When you turn on a new destination, theStacc automatically turns the previous one off — your old connection settings are kept, so you can switch back later without re-entering anything. The Publishing page says it plainly under Destination: *"One destination is active at a time."*
You need to be a project admin to connect, switch, or test a destination. Connecting a platform, toggling it on or off, editing its settings, and running a test are all admin-only actions. If you're a member without admin rights, the controls are visible but disabled. (Ask your project owner or an admin to make the change, or to grant you admin on the project.)
With those out of the way, here's each destination.
WordPress.com#
Hosted WordPress sites. Recommended for WordPress.com users — no passwords to copy, just a secure sign-in.
- Click Connect WordPress.com.
- You'll be redirected to WordPress.com to securely authorize access (OAuth).
- Sign in and grant theStacc access.
- Select your target site if your account has more than one.
Each published blog includes the title, body content (HTML), featured image, categories, tags, SEO meta data (Yoast/RankMath compatible), and post slug.
If the connection ever stops working, you'll see *"WordPress.com token expired or invalid. Please reconnect your account."* — click Connect again to re-authorize.
WordPress.org#
Self-hosted WordPress sites. Connect with an Application Password — a WordPress feature made specifically for apps like theStacc, so you never share your real login password.
- In your WordPress admin, go to Users > Profile > Application Passwords.
- Create a new password and name it
theStacc. - Copy the generated password.
- In theStacc, click Connect WordPress.org (self-hosted), then enter your site URL, your WordPress username (your login username, not your display name), and the application password.
- theStacc tests the connection as you save. If it succeeds, you're connected; if not, you'll get a specific reason (see the error table below).
Requires WordPress 5.6+ with the REST API enabled (on by default). HTTPS is recommended.
About the application password field. WordPress shows application passwords as groups separated by spaces (for example abcd 1234 efgh 5678 ijkl 9012). You can paste it with or without the spaces — theStacc strips them for you. After stripping spaces it must be 8 to 200 characters and contain only letters and numbers. If it contains special characters, theStacc rejects it on the spot, because that almost always means you pasted your normal WordPress login password by mistake. Generate a real Application Password from Users > Profile > Application Passwords instead.
Publishing permission. The WordPress user must be an Administrator, Editor, or Author — those are the roles that can create posts. If the user can sign in but can't publish, theStacc tells you the exact role it found and which roles are allowed.
Custom Webhook#
Send blog posts to any platform via HTTP. Works with any system that accepts HTTP POST requests — a custom CMS, a static-site build pipeline, an internal tool, or a no-code automation.
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.
- Click Add Webhook.
- Enter your Webhook URL.
- Add an optional Secret Key — when set, theStacc signs every request so your receiver can verify it really came from theStacc. The secret must be at least 16 characters (and at most 256).
- Add custom headers (optional) — for authentication tokens, API keys, or routing.
- Click Test Connection to verify the URL is reachable.
- 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(orpublished_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 asexternal_post_idso 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 });
}Verify webhook signatures (recommended)#
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 eventPython / 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 eventPHP:
<?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 eventIf 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.
| Event | When fired | Payload |
|---|---|---|
test.ping | Test Connection button | {event, message, timestamp} |
blog.published | First successful publish | full blog payload (see above) |
blog.updated | Re-publish of an existing blog | full blog payload — same shape as blog.published |
blog.unpublished | User unpublishes from theStacc | {event, blog_id, title} |
blog.deleted | User 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_idas a dedupe key. If theStacc ever resends (manual user retry), you'll see the sameblog_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-Signatureheader on every request when a secret is set. - Idempotency — use
blog_idas 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.
- Click Connect Ghost.
- Enter your Ghost site URL (for example
https://myblog.com). - Enter your Ghost Admin API key. Create one in Ghost Admin > Settings > Integrations > Add custom integration — the Admin API key looks like
{id}:{secret}. - theStacc tests the connection as you save.
Posts are created with full content, featured images, tags, and meta data.
Key format. The Admin API key must be in {id}:{secret} form and 20 to 200 characters long. The {secret} half must be valid hexadecimal — that's how Ghost keys are issued — and theStacc uses it to sign a short-lived token when it talks to your site. If the format is off, you'll see *"Invalid API key format. Expected format: {id}:{secret}"* or *"Invalid API key secret — must be a valid hex string."*
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#
- In Webflow, open the site you want to connect.
- Go to Site Settings → Apps & Integrations → API Access. You'll see this:

- 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.
- On the next screen, name the token
theStaccand 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.
| Scope | Permission | Underlying API scope | Why |
|---|---|---|---|
| CMS | Read and write | cms:read + cms:write | Create and update blog items in your collection |
| Sites | Read and write | sites:read + sites:write | Read site info (for the live URL) and re-publish the site so new posts appear live |
| Custom code | None | — | Not used |
| Pages | None | — | Not used |
| Users | None | — | Not used |
| Forms | None | — | Not used |
| Components | None | — | Not used |
| Ecommerce | None | — | Not used |
| Authorized user | Read only | authorized_user:read | Default — 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.
- Click Generate token and copy it immediately — Webflow won't show it again. The token must be 10 to 500 characters (real Webflow tokens fall well within that range).
Step 2 — Connect in theStacc#
- In theStacc, open Content SEO → Settings → Publishing and toggle Webflow on.
- Paste the token, give the integration a friendly name, and click Continue.
- Select your Webflow site from the dropdown.
- Select the CMS collection where blog posts should land.
- 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.
- 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. This is the default for a new Webflow connection.
- 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.
- Click Create Integration.
Changing Webflow settings later (no token re-entry)#
Open the connected Webflow integration's Edit settings to change the publishing behavior, the selected site or collection, or your field mappings — without re-pasting the API token. For security, theStacc masks the saved token and never shows it back to you, so the edit form simply leaves it untouched unless you deliberately paste a new one.
If you do paste a new token, theStacc validates it against Webflow before saving. A typo or a revoked token is rejected with a clear message (*"Webflow API token is invalid or expired…"* or *"…lacks required scopes (sites:read, cms:read, cms:write)"*) so a bad token can never silently replace your working one.
Recommended CMS collection fields#
| Field | Webflow type |
|---|---|
| Name | Plain text (auto-created — title goes here) |
| Slug | URL slug (auto-created) |
| Post Content | Rich text |
| Featured Image | Image |
| Excerpt | Plain text |
| Meta Title | Plain text |
| Meta Description | Plain text |
| Tags | Plain text (comma-separated) |
| Author | Plain 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 (Beta)#
Publish blog posts to your Shopify store's built-in blog. Shopify support is currently Beta.
- Click Connect Shopify.
- Enter your store domain — you can type just the handle (
your-store), the fullyour-store.myshopify.com, or even a pasted store URL. theStacc normalizes whatever you enter to the canonicalyour-store.myshopify.comform. (You'll find this in your Shopify admin URL.) - You're redirected to Shopify to approve access (OAuth). theStacc never sees your Shopify password.
- After you approve, theStacc connects, finds the blog to publish to, and makes Shopify the active destination. If your store has no blog yet, theStacc creates one named News so there's somewhere for posts to land.
Posts are published as Shopify blog articles with content, images, and tags, via Shopify's GraphQL Admin API. As with every destination, connecting Shopify deactivates whatever was active before — one destination at a time per project.
If the connection is lost (for example you uninstall the app on Shopify's side), you'll see *"Shopify access token is invalid or expired. Please reconnect."* — click Connect Shopify again to re-authorize.
Zepio#
Publish blog posts directly to your Zepio website using Zepio's Developer API.
What you'll need:
- Your Zepio website URL
- A Zepio Developer API key with the "Create blog posts" permission
Steps:
- In Zepio, go to Zepio Admin → Settings → Developer API and create an API key with the "Create blog posts" permission.
- In theStacc, click Connect Zepio.
- Enter your Zepio Website URL (for example
https://mybusiness.com). - Paste your Developer API key.
- Click Save Integration. theStacc verifies the key with Zepio before saving — if the key is wrong or lacks permission, it tells you immediately rather than failing silently at publish time.
The key is 10 to 500 characters. Posts are created with title, content, featured image, excerpt, and SEO meta data.
Editing later — rotate the key or change the URL. Once Zepio is connected, an Edit button (tooltip: *"Update site URL or rotate the API key"*) lets you change the site URL or paste a new Developer API key without disconnecting and reconnecting from scratch. For security theStacc never shows the saved key back to you, so re-enter it (or paste a fresh one) when you save changes.
Note on editing published posts. Zepio's Developer API can create a post but has no "update" endpoint, so once a post is published to Zepio you edit it from your Zepio dashboard, not from theStacc. Re-publishing the same post from theStacc isn't supported for Zepio for the same reason.
Status and diagnostics#
Every connected integration carries connection-health information theStacc updates each time it tests or publishes:
- Last tested — when the connection was last checked.
- Last test success — whether that most recent check passed or failed.
- Connection error — the specific reason the last check failed (shown when you connect or test). This is the message to read first when something isn't working.
Use the Test Connection button (admin only) any time to re-run the check on demand. WordPress, Ghost, and Zepio are also tested automatically the moment you connect them, so a bad credential is caught up front instead of at the next scheduled publish.
Common error messages#
| Message you might see | What it means | How to fix it |
|---|---|---|
| *"This looks like a regular WordPress password, not an Application Password."* | You pasted your WordPress login password into the app-password field. | Generate an Application Password in Users > Profile > Application Passwords and paste that instead. |
| *"WordPress REST API not found at this URL"* / *"REST API is blocked"* | The site URL is wrong, or a security plugin / server rule is blocking the WordPress REST API. | Check the URL, and allow REST API access in your WordPress security settings. |
| *"Invalid API key format. Expected format: {id}:{secret}"* (Ghost) | The Ghost Admin API key isn't in {id}:{secret} form, or its secret isn't valid hex. | Copy the Admin API key (not the Content API key) from your Ghost custom integration. |
| *"Invalid Developer API key"* / *"…make sure it has the 'Create blog posts' permission"* (Zepio) | The Zepio key was rejected, or it lacks the required permission. | Create a key with the "Create blog posts" permission and rotate it via Edit. |
| *"Webflow API token is invalid or expired"* / *"…lacks required scopes"* | The Webflow token is wrong, revoked, or missing CMS/Sites scopes. | Generate a fresh token with CMS: Read and write and Sites: Read and write. |
| *"WordPress.com token expired"* / *"Shopify access token is invalid or expired"* | An OAuth connection was revoked or aged out. | Click Connect again to re-authorize the account. |
| *"Your Zepio site is rate-limiting requests right now"* (HTTP 429) | The destination is temporarily throttling — not an auth problem. | Wait a few minutes and try again. Don't rotate a working key over this. |
| *"Webhook returned redirect … — webhook receivers must respond with 2xx directly"* (3xx) | Your webhook receiver answered with a redirect, which theStacc rejects for security. | Make your receiver return a 2xx status directly instead of redirecting. |
Field and credential limits at a glance#
| Field | Limit |
|---|---|
| Integration name | up to 100 characters |
| WordPress.org application password | 8–200 characters, letters and numbers only (special characters rejected as a likely login password) |
| Webhook secret | at least 16 characters (HMAC signing) |
| Webflow API token | 10–500 characters |
| Ghost Admin API key | {id}:{secret} format, 20–200 characters |
| Zepio Developer API key | 10–500 characters |
Managing connections#
Each connected integration shows:
- Status — active or inactive.
- Active/Inactive toggle — turn this destination on or off. Turning one on automatically turns the others off (one active destination per project).
- Test button — re-check that the connection works.
- Edit — update settings or rotate credentials (available for Zepio, Webflow, and the other credential-based platforms) without a full reconnect.
- Disconnect — remove the integration entirely.
Related#
- Publishing — choose your publishing mode (auto-publish, require approval, or draft) and learn how a generated post reaches your site.
- Webhook Reference — the full developer contract for the Custom Webhook destination, including the timeout, retry, and redirect behavior that determines when a publish is marked failed.
- Deploy Hooks — trigger a static-site rebuild after each publish so new posts appear on the live site.
Publishing errors & retries. theStacc does not silently retry a failed publish — a timeout, a 5xx, or a rejected redirect fails the publish and surfaces a "Failed to publish" message with the reason. The exact contract (15-second timeout, no automatic retries, 3xx rejected) lives in the Webhook Reference; for non-webhook destinations, fix the connection error shown above and re-publish.