Agentic Mode

API Documentation

Connect your AI agent to Distribb. You write the content — we handle SEO optimization, backlinks, publishing, and analytics.

Base URL  https://distribb.io/api/v1

Quick Start

Fastest setup (Cursor, Claude Code, Codex, and 45+ agents)

npx skills add Bomx/distribb-skill
  1. Sign up at distribb.io
  2. Find your API key in Settings
  3. Set it as an environment variable: export DISTRIBB_API_KEY=your_key
  4. Install the skill: npx skills add Bomx/distribb-skill
  5. Or use curl directly: curl -s -H "Authorization: Bearer $DISTRIBB_API_KEY" https://distribb.io/api/v1/projects | jq .

Authentication

All API requests require a Bearer token in the Authorization header. Your API key is available in your account settings.

Authorization: Bearer your_api_key_here
GET /projects List all your projects
30 req/min

Returns all active projects linked to your account. Use the project ID in subsequent API calls.

Response
{ "projects": [ { "ID": 1, "BusinessName": "Acme Corp", "WebsiteUrl": "https://acme.com", "BusinessDescription": "...", "Language": "English (US)", "Status": "Active", "BacklinkCredits": 10, "ArticlesPerDay": 1 } ] }

New-client flow (agencies)

Spinning up a client from scratch is a deliberate 3-call sequence. Creating the project does NOT spend keyword-research credits, so you can create and configure freely, then ask the user before starting research.

  1. POST /projects with the client's website_url (plus any settings) to create and configure the project.
  2. POST /projects/:id/onboarding to start keyword research and the first articles. Ask the user first; this one spends credits.
  3. POST /projects/:id/wordpress to connect the client's CMS so articles can publish.
POST /projects Create + configure a project
10 req/min

Create a new project for the authenticated account. Only website_url is required; you can pass any writable field from PUT /projects/:id in the same body to configure the project on create. Creating a project does NOT start keyword research (that spends credits), so after creating, ask the user, then call POST /projects/:id/onboarding.

JSON Body

FieldTypeRequiredDescription
website_urlstringYesClient site URL (normalized and validated)
business_namestringNoBusiness name
business_descriptionstringNoWhat the business does
target_audiencestring[]NoAudience segments
(any writable field)anyNoAny key from the PUT writable set (same validation)
Response (201)
{ "project_id": 42, "website_url": "https://client.com", "backlink_credits": 100, "project_slots": { "used": 3, "total": 5 }, "message": "Project created. Keyword research has NOT started yet.", "next_step": "Ask the user if they want to start keyword research now...", "settings_applied": ["tone", "language"] }
Response (200, idempotent reuse)

Posting the same website_url again returns the existing active project instead of creating a duplicate.

{ "project_id": 42, "website_url": "https://client.com", "already_existed": true, "message": "A project already exists for this website; returning it." }
Response (402, project limit reached)

Project slots are gated by the account's paid quantity. At the limit, this returns a machine-readable body with a purchase_url the human can click to buy another slot; retry the same body once they confirm.

HTTP/1.1 402 Payment Required { "error": "project_limit_reached", "active_projects": 5, "project_quantity": 5, "purchase_url": "https://distribb.io/dashboard?add_project=1", "instructions_for_agent": "Tell the user they've hit their project limit and share purchase_url..." }
GET /projects/:id Read one project's full settings
30 req/min

Returns the project plus a settings object that mirrors the writable keys of PUT /projects/:id. GET it, tweak keys, and PUT the same shape back (read-modify-write).

Response
{ "project": { "ID": 42, "BusinessName": "Acme Corp", "WebsiteUrl": "https://acme.com", ... }, "settings": { "tone": "Informative", "language": "English (US)", "keyword_region": "United States", "internal_links_per_article": 5, "competitors": ["https://competitor.com"], "duplicate_content_protection": true, "articles_per_day": 1 } }
PUT /projects/:id Update project settings
10 req/min

Accepts the same field set as the Settings UI. Send only the fields you want to change; article-quality and image preferences are MERGED, so a partial update never resets the others. GET this project to see the settings object; every key there is writable here.

Watch the exact key names: the tone key is tone (not content_style), the duplicate-content toggle is duplicate_content_protection (not a bare duplicate_content_guard), and articles_per_day is plan-controlled and NOT settable (it is echoed back under ignored, never written).

Writable fields (JSON body)

FieldTypeNotes
ai_instructionsstringCustom writer instructions (null to clear)
business_namestringBusiness name
business_descriptionstringWhat the business does
target_audiencestring[]Audience segments
sitemap_urlstringSitemap URL
blog_root_urlstringBlog root URL
content_pillarsstring[]Pillar page URLs (list or CSV)
internal_links_per_articleint1 to 5 (alias: internal_links)
toneenumInformative, Conversational, Persuasive
languagestringSupported label, e.g. "English (US)", "French"
keyword_regionstringe.g. "United States", "United Kingdom", "Worldwide"
writing_profileenumExperienced practitioner, Simple educational, Balanced SEO
product_positioningenumNeutral operational, Soft mention, Promotional
custom_author_namestringByline author name
social_media_ai_instructionsstringSocial repurposing instructions
publish_timestring24-hour "HH:MM", e.g. "09:00"
timezonestringIANA name, e.g. "America/New_York"
publishing_statusenumPublish Immediately, Save as Drafts, Send as Drafts
social_media_publishing_statusenumPublish Immediately, Save as Drafts
image_hostingenumDistribb, CMS
image_stylestringFree-form style description (null to clear)
brand_colorstringHex, e.g. "#e11d2a"
image_prompt_instructionsstringExtra image-prompt guidance
title_based_featured_imageboolRender the title onto the featured image
cta_intensityenumNone, Soft, Direct
first_person_writingboolWrite in first person
table_of_contentsboolAdd a table of contents
avoid_formulaic_section_endingsboolSuppress formulaic section closers
require_operational_examplesboolRequire concrete operational examples
strict_banned_phrase_guardboolEnforce banned phrases strictly
banned_phrasesstring[]Phrases to avoid (list or newline text)
brand_intelligenceboolBrand intelligence on/off
duplicate_content_protectionboolDuplicate-content guard on/off
videos_enabledboolEmbed YouTube videos on/off
backlinks_networkboolParticipate in the backlink network
competitorsstring[]Competitor domains (list or CSV)

Not settable via API: articles_per_day (plan-controlled) and optimization thresholds (applied at scan time). Passing them is accepted but echoed under ignored. Connect WordPress via POST /projects/:id/wordpress.

Response
{ "project_id": 42, "updated_fields": ["tone", "duplicate_content_protection"], "updated_columns": ["ContentStyle", "DuplicateContentGuardEnabled"], "message": "Project settings updated.", "ignored": ["articles_per_day"], "notes": { "articles_per_day": "Controlled by the plan; not settable via the API." } }
POST /projects/:id/onboarding Start keyword research + first articles
5 req/min

Runs the same discovery pipeline the dashboard runs when onboarding finishes: keyword research, then the first planned articles. Ask the user before calling this; it spends keyword and LLM credits. Idempotent: if the project already has articles it will not re-run. Free and Agentic plans bring their own keywords, so research is skipped and the response says so.

Response (202)
{ "project_id": 42, "status": "started", "message": "Keyword research started. Your content calendar will fill shortly...", "poll": "/api/v1/articles?project_id=42" }
Response (200, skipped)
{ "project_id": 42, "status": "skipped_byok", "message": "Your plan brings its own keywords... Use POST /api/v1/keywords/search." }
POST /projects/:id/wordpress Connect a WordPress site
10 req/min

Connect (or reconnect) a project's WordPress site via the Distribb plugin. Install the Distribb WordPress plugin on the site, copy its Integration Key, and send it here. Credentials are validated before saving (format check plus live probe), exactly like the Settings UI.

JSON Body

FieldTypeRequiredDescription
wordpress_urlstringYesThe WordPress site URL
integration_keystringYesThe Distribb plugin Integration Key
wp_usernamestringNoOptional WordPress username
Response
{ "project_id": 42, "wordpress_url": "https://client.com", "status": "connected", "message": "WordPress connected successfully." }
GET /articles List articles with filters
30 req/min

Query Parameters

NameTypeRequiredDescription
project_idintNoFilter by project
statusstringNoDraft, Planned, Published, Generating
limitintNoMax results (default 50, max 200)
offsetintNoPagination offset
Response
{ "articles": [ { "ID": 42, "Title": "Best CRM for Startups", "MainKeyword": "best crm for startups", "Status": "Draft", "ScheduledDate": "2026-03-25T09:00:00", "ProjectID": 1, "Slug": "best-crm-for-startups", "ArticleStyle": "Informative" } ], "count": 1 }
POST /articles Submit a new article
10 req/min

Submit AI-generated content. Distribb handles backlink credit processing, stores the article, and prepares it for publishing.

JSON Body

FieldTypeRequiredDescription
project_idintYesTarget project ID
keywordstringYesMain keyword / topic
titlestringNoArticle title (defaults to keyword)
contentstringNoFull HTML content
meta_descriptionstringNoSEO meta description
scheduled_datestringNoISO 8601 date (e.g. 2026-03-25T09:00:00Z)
article_stylestringNoInformative, Listicle, How-To, etc.
statusstringNoDraft (default) or Planned
Response (201)
{ "article_id": 123, "status": "Draft", "keyword": "best crm for startups", "slug": "best-crm-for-startups", "message": "Article created as Draft.", "backlinks_processed": 2 }
PUT /articles/:id Update an existing article
10 req/min

Update an article's content, title, meta description, status, or scheduled date. Useful for revising articles to add backlink targets after receiving a backlinks_warning. Cannot update published articles.

Body Parameters
contentstringUpdated HTML content
titlestringUpdated title
meta_descriptionstringUpdated meta description
statusstring"Draft" or "Planned"
scheduled_datestringISO 8601 date (e.g. 2026-04-01T09:00:00Z)

Send only the fields you want to update. If content is updated and the project participates in the backlink network, backlinks are re-scanned.

Response
{ "article_id": 123, "updated_fields": ["Content", "IsPreGenerated"], "message": "Article updated successfully.", "backlinks_processed": 2 }
GET /articles/:id Get a single article
30 req/min

Retrieve full article details including content. The article must belong to your account.

Response
{ "ID": 123, "Title": "Best CRM for Startups", "MainKeyword": "best crm for startups", "Content": "<h1>Best CRM for...</h1>...", "MetaDescription": "Compare the top CRM...", "Status": "Draft", "Slug": "best-crm-for-startups", "ScheduledDate": "2026-03-25T09:00:00", "ProjectID": 1, "ArticleStyle": "Informative" }
DELETE /articles/:id Delete a draft or planned article
10 req/min

Delete an article that belongs to your account. Draft and Planned articles are removed. Published articles are blocked (400); unschedule them, or unpublish or hide them from the dashboard or your CMS first.

Response
{ "article_id": 123, "deleted": true, "message": "Article deleted." }
POST /keywords/search Search keywords with volume data
5 req/min

Search for keyword ideas with search volume and difficulty data. Agentic Mode uses Distribb's keyword data. Legacy Free Agentic accounts use their own DataForSEO or Ahrefs keys (see BYO Keys below). Also available at the alias POST /keywords/research (identical behavior).

JSON Body

FieldTypeRequiredDescription
keywordstringYesSeed keyword to research
project_idintNoProject for context and ownership
Response
{ "keywords": [ { "keyword": "crm software for small business", "search_volume": 2400, "keyword_difficulty": 38 } ] }
Bring-Your-Own-Keys (legacy Free Agentic accounts)

The Free Agentic plan is deprecated and no longer offered to new users. Current plans are Agentic Mode at $49/month and Pro at $97/month. If the calling user is on a legacy Free Agentic account and has not yet saved a DataForSEO or Ahrefs API key, this endpoint returns HTTP 402 Payment Required with a machine-readable body so your agent knows what to do. Paid plans never see this response.

HTTP/1.1 402 Payment Required { "error": "byo_keys_required", "message": "Keyword research requires your own DataForSEO or Ahrefs API key.", "plan": "Agentic Free", "required": { "any_of": ["dataforseo", "ahrefs"] }, "setup_url": "https://distribb.io/settings#seo-keys", "docs_url": "https://distribb.io/api-docs#byo-keys", "instructions_for_agent": "Tell the user to add their DataForSEO Login + API Key (or Ahrefs API Key) at distribb.io/settings, then re-run keyword research." }

Agent contract: on receiving 402 with error = "byo_keys_required", halt the keyword-research step and surface instructions_for_agent verbatim to the human user. Do not retry until setup_url has been visited and credentials saved.

Provider precedence when both keys are saved: DataForSEO is used first (full keyword expansion); if only Ahrefs is saved, the response is sourced from Ahrefs Keywords Explorer ("source": "byo_ahrefs"). All other Distribb endpoints (articles, integrations, backlinks) work normally without BYO keys.

GET /integrations List connected CMS and social platforms
30 req/min

Lists active CMS and social integrations. Each row returns Platform (the platform/CMS type, e.g. WordPress, Shopify, Google Search Console, a social type), a friendly IntegrationName label, and Status.

Query Parameters

NameTypeRequiredDescription
project_idintNoFilter to a specific project
Response
{ "integrations": [ { "ID": 1, "Platform": "WordPress", "IntegrationName": "WordPress", "Status": "Active", "ProjectID": 1, "BusinessName": "Acme Corp" } ] }
POST /articles/generate Expand your content into a full article (Pro only)
5 req/min

Submit your own content (notes, drafts, talking points) and Distribb's AI will expand it into a full SEO-optimized article with YouTube videos, images, quotes, backlinks, and internal links. Requires the Pro plan and costs 1 article credit. Not available on the Agentic plan (use POST /articles instead to submit your own AI-generated content).

JSON Body

FieldTypeRequiredDescription
project_idintYesTarget project ID
keywordstringYesTarget keyword / topic for SEO
source_contentstringYesYour content to expand (notes, draft, talking points, etc.)
instructionsstringNoAdditional guidance (e.g. "add YouTube videos", "focus on beginners")
titlestringNoArticle title (defaults to keyword)
article_stylestringNoInformative, Listicle, How-To, etc. (default: Informative)
Response (202)
{ "article_id": 456, "status": "generating", "keyword": "link building strategies", "slug": "link-building-strategies", "message": "Article generation started. Distribb will expand your content...", "article_credits_remaining": 29 }
POST /articles/:id/publish Publish to connected CMS
5 req/min

Triggers CMS publishing for an article. Distribb handles the integration (WordPress, Webflow, Shopify, Ghost, Wix, Notion, Framer, or Webhook) based on the project's connected platform.

Response (200)
{ "status": "published", "article_id": 123 }
Response (202 - Queued for Retry)
{ "error": "Publishing failed. The article has been queued and will be retried.", "article_id": 123 }
GET /business-context Get project business details
30 req/min

Returns project-specific context needed for high-quality content: business name, description, competitors, custom AI instructions, and language. Use this to ground your AI writer in the user's brand voice.

Query Parameters

NameTypeRequiredDescription
project_idintYesYour project ID
Response
{ "business_name": "Acme Corp", "website_url": "https://acme.com", "description": "CRM platform for startups...", "competitors": ["https://competitor1.com", "https://competitor2.com"], "ai_instructions": "Use a friendly tone, focus on SaaS...", "language": "English (US)", "target_audience": "SaaS founders, startup CTOs", "internal_links_per_article": 5 }
GET /search-console GSC search performance by query and page
10 req/min

Google Search Console performance for a project: top queries, top pages, and site totals (clicks, impressions, CTR, average position) over the last N days. Requires the user to have connected GSC (Integrations, Google Search Console). If GSC is not connected, returns HTTP 200 with connected: false plus an instructions_for_agent string telling the user to connect it.

Query Parameters

NameTypeRequiredDescription
project_idintYesYour project ID
daysintNoLookback window (default 28, max 90)
limitintNoRows per dimension (default 25, max 1000)
start_rowintNoPagination offset; see the pagination block
compareboolNoAdd period-over-period deltas (see below)

Each query/page row carries is_brand (matches the business name) and striking_distance (impressions ≥ 10 and average position 4 to 20). Paginate with start_row; the response returns a pagination block whose next_start_row is null on the last page. With compare=true, each row adds delta_clicks, delta_impressions, delta_position, and is_new versus the immediately preceding window, and a top-level comparison block adds previous_totals and delta_totals.

Response
{ "connected": true, "project_id": 1, "property": "https://acme.com/", "date_range": { "start_date": "2026-06-07", "end_date": "2026-07-05", "days": 28 }, "totals": { "clicks": 1240, "impressions": 88200, "ctr": 0.0141, "avg_position": 18.4 }, "top_queries": [ { "query": "best crm for startups", "clicks": 92, "impressions": 4100, "ctr": 0.0224, "position": 7.2, "is_brand": false, "striking_distance": true } ], "top_pages": [ "..." ], "pagination": { "limit": 25, "start_row": 0, "next_start_row": 25 } }
With ?compare=true (adds)
{ "comparison": { "previous_date_range": { "start_date": "2026-05-10", "end_date": "2026-06-06", "days": 28 }, "previous_totals": { "clicks": 1010, "impressions": 80100, "ctr": 0.0126, "avg_position": 19.1 }, "delta_totals": { "clicks": 230, "impressions": 8100, "ctr": 0.0015, "avg_position": -0.7 } } }
GET /rankings  ·  /analytics Aliases of /search-console
10 req/min

Documented aliases of /search-console, for agents that look for /rankings or /analytics. Same handler, same parameters, same response. Note: this is Search Console search performance (clicks, impressions, CTR, position by query and page), NOT web-session analytics.

GET /ai-visibility AI-visibility (AEO) data by view
30 req/min

Read AI-visibility data for a project: how often it is cited across AI answer engines, its share of voice, and tracked prompts. API-key mirror of the dashboard AI-visibility pane.

Query Parameters

NameTypeRequiredDescription
project_idintYesYour project ID
viewstringNosummary (default), prompts, competitors, cited_pages
page, per_pageintNoPagination for view=prompts
Response (view=summary)
{ "visibility_score": 42, "share_of_voice": 0.18, "engines": [ { "engine": "perplexity", "cited": true } ], "manual_scans_used": 1, "manual_scans_limit": 2, "can_scan": true }
POST /ai-visibility/scan Trigger an on-demand scan
6 req/min

Trigger an on-demand AI-visibility scan (the "Scan now" action). Enforces a per-project daily manual-scan cap that is SHARED with the dashboard button and the Distribb Agent, so heavy API scanning draws from the same budget. Returns 202 when queued, 429 when the cap is hit. project_id in the body or query.

Response (202, queued)
{ "status": "queued", "project_id": 1, "manual_scans_used": 2, "manual_scans_limit": 2, "message": "Scan queued. Poll GET /api/v1/ai-visibility?view=summary until it completes." }
Response (429, rate limited)
HTTP/1.1 429 Too Many Requests { "status": "rate_limited", "manual_scans_used": 2, "manual_scans_limit": 2, "error": "Daily scan limit reached (2/2). Resets at midnight UTC." }
POST /ai-visibility/prompts Add or remove a tracked prompt
20 req/min

Add (POST) or remove (DELETE) a tracked AI-visibility prompt. Added prompts are scanned on the next scan; removal is a soft-delete (past results are kept).

JSON Body

FieldTypeRequiredDescription
project_idintYesYour project ID
promptstringYesThe prompt to track (or remove)
Response
{ "status": "added", "prompt": "best crm for startups" }
GET /suggestions List content-optimization suggestions
30 req/min

List a project's content-optimization suggestions (top queries and pages where a rewrite could lift rankings). Returns per-status counts, the project's suggestion settings, and whether GSC is connected. Suggestions are largely GSC-driven, so a disconnected project will usually have none.

Query Parameters

NameTypeRequiredDescription
project_idintYesYour project ID
statusstringNopending, approved, rewriting, ready, published, rejected, failed, superseded
typestringNoFilter by opportunity_type: cannibalisation, declining_page, striking_distance, ctr_underperform, etc.
limitintNoMax results (default 100, max 500)
Response
{ "project_id": 1, "suggestions": [ { "id": 7, "status": "pending", "article_title": "Best CRM for Startups" } ], "counts": { "pending": 3, "ready": 1, "published": 5 }, "gsc_connected": true }
POST /suggestions/run Trigger an on-demand suggestion scan
3 req/min

Pull GSC, score articles, and insert new pending suggestions (deduped against existing open ones). Mirrors the weekly Monday cron. Returns the count created. project_id in the body or query.

Response
{ "project_id": 1, "created": 4, "message": "4 new suggestion(s) created. List them with GET /api/v1/suggestions?..." }
POST /suggestions/:id/… Read + act on a single suggestion
5-30 req/min

The full review-and-publish loop for one suggestion. The typical path is approve (starts a background rewrite), poll the single-suggestion GET until status is ready, review the diff, then publish.

Endpoints

Method + PathDescription
GET /suggestions/:idGet one suggestion, including proposed_diff once a rewrite is ready
GET /suggestions/:id/diffBefore/after rewrite plus the GSC trigger_snapshot (null until ready)
POST /suggestions/:id/approveApprove a pending suggestion; kicks off scrape + LLM rewrite. Poll until ready
POST /suggestions/:id/rejectReject so it stops showing as actionable. Optional body { "reason": "..." }
POST /suggestions/:id/publishPublish a ready rewrite to the connected CMS (409 if the article changed since staging)
POST /suggestions/:id/regenerateRe-run the rewrite with optional { "feedback": "..." }. Paid plans only (402 on free)
Response (approve)
{ "suggestion": { "id": 7, "status": "approved" }, "message": "Approved. A background rewrite has started; poll until status is 'ready', then publish it." }
GET /microworkers/campaigns List registered Microworkers campaigns
30 req/min

List project-scoped Microworkers campaigns registered through Distribb.

Query Parameters

NameTypeRequiredDescription
project_idintNoFilter to a specific project
limitintNoMax results (default 50, max 100)
offsetintNoPagination offset
Response
{ "campaigns": [ { "ID": 3, "ProjectID": 1, "MicroworkersCampaignID": "abc123", "Platform": "linkedin", "Title": "Share our article", "Status": "running", "AvailablePositions": 50, "PaymentPerTask": 0.15 } ], "count": 1 }
POST /microworkers/campaigns Create + manage Microworkers campaigns
5-30 req/min

Create a Microworkers Basic Campaign and register it to a project, or register/inspect an existing one. Creating a campaign also builds its Microworkers template.

Endpoints

Method + PathDescription
POST /microworkers/campaignsCreate + register. Body: project_id, title, description, template_html (plus optional available_positions, payment_per_task ≥ 0.15, etc.)
POST /microworkers/campaigns/registerRegister an existing campaign by campaign_id so this key can manage it
GET /microworkers/campaigns/:idGet a registered campaign plus its live Microworkers status
GET /microworkers/campaigns/:id/slotsList slots/submissions (query: page, pageSize, status)
POST /microworkers/slots/:id/rateRate a slot. Body: campaign_id, rating (OK, NOK, REVISE), optional comment
Response (201, create)
{ "campaign_id": "abc123", "template_id": "tpl_456", "project_id": 1, "status": "created", "external_url": "https://ttv.microworkers.com/campaign/abc123", "message": "Microworkers campaign created and registered to the project." }
GET /outreach/entitlement Check Outreach Pro entitlement
30 req/min

Returns whether the authenticated account is entitled to Outreach Pro (the flag is set and the account is active). Returns 403 when the Outreach Pro beta is closed for the account.

Response
{ "entitled": true, "plan": "Outreach Pro", "status": "active" }
POST /outreach/sync Mirror the Outreach skill DB
10 req/min

Idempotent upsert of the local Outreach skill database into MySQL mirror tables, keyed by the authenticated account. Body: { since, persons[], identities[], leads[], actions[], suppressions[] }. The client re-pushes boundary rows; the upsert makes it idempotent. Returns per-table counts. 403 if not entitled.

Response
{ "synced": { "persons": 12, "identities": 3, "leads": 40, "actions": 88, "suppressions": 5 } }
POST your_webhook_url Payload Distribb sends to your endpoint

A custom Webhook integration receives initial articles and later edits at the same URL, using the same access token. Handle publish_articles and update_articles separately. Your endpoint must accept this JSON shape, validate the bearer token, and respond within 30 seconds. Use 202 if you queue the work.

Headers

NameValue
Content-Typeapplication/json
AuthorizationBearer <your access token> — the token you set in Settings → Integrations → Webhook
X-API-Key<your access token> — same value, sent for receivers that read this header (AWS API Gateway, generic SaaS)
x-make-apikey<your access token> — same value, sent for Make.com Custom Webhook "API Key restriction"
User-AgentDistribb-Publisher/1.0
Idempotency-KeyA unique delivery key, reused for retries of that delivery. Later edits have a new key even when the article ID stays the same.

Distribb sends your access token under three header names so the request authenticates against any common webhook receiver without extra configuration. Your endpoint only needs to validate one of them.

Body

FieldTypeDescription
event_typestring"publish_articles" on the first send for a given slug; "update_articles" when the user clicks Sync to CMS on an article that is already live. Route on this field to decide CREATE vs UPDATE on your side.
timestampstringWhen the webhook was sent (ISO 8601, UTC)
data.articles[].idstringStable Distribb article ID. Store it as a unique post identifier. Do not discard all later deliveries with the same ID: they can contain edits.
data.articles[].titlestringArticle title
data.articles[].slugstringURL slug, e.g. lawn-care-toronto. A fallback to match older posts without a stored Distribb ID. Preserve the existing permalink on content updates.
data.articles[].url, published_urlstring, optionalThe existing stored public URL. An update can identify the article by ID even if its URL is unknown.
data.articles[].external_idstring, optionalThe remote post ID, when known.
data.articles[].content_htmlstringFull article body as HTML (with <h2>, <p>, <ul>, <figure>, etc.). Render this if your CMS displays HTML directly.
data.articles[].content_markdownstringSame body converted to real Markdown. Render this if your CMS expects Markdown. Pick one of the two — never both.
data.articles[].meta_descriptionstringSEO meta description (~155 chars)
data.articles[].created_at, published_atstring, optionalOriginal dates (ISO 8601, UTC). Unknown dates are omitted on updates; retain your stored dates when a field is absent.
data.articles[].updated_atstringSaved modification time, or delivery time when no modification date is available.
data.articles[].image_urlstringPublic URL of the feature image (already hosted on our CDN). Use as-is or re-upload to your storage.
data.articles[].alt_textstringAlt text for the feature image
data.articles[].tagsstring[]SEO tags / keywords
data.articles[].authorstringAuthor display name
data.articles[].statusstring"Published" or "Draft". Sync preserves the article’s current publication status.
data.articles[].is_updatebooleantrue on sync events, false on first publish. Redundant with event_type but handy if your router checks the article object directly.
data.articles[].update_onlybooleantrue on updates. If the existing post is missing, return an error and create nothing.
Example payload — initial publish
{ "event_type": "publish_articles", "timestamp": "2026-04-18T15:21:00Z", "data": { "articles": [ { "id": "63452", "title": "Essential Lawn Maintenance Toronto Guide 2026", "slug": "essential-lawn-maintenance-toronto-2026", "content_html": "<h2>Why a Healthy Lawn Matters</h2><p>Toronto lawns face...</p>...", "content_markdown": "## Why a Healthy Lawn Matters\n\nToronto lawns face...", "meta_description": "A practical guide to year-round lawn care in Toronto...", "created_at": "2026-04-15T12:23:16Z", "image_url": "https://rebelgrowth.s3.us-east-1.amazonaws.com/blog-images/lawn-1.jpg", "alt_text": "lawn maintenance Toronto", "tags": ["lawn care", "toronto", "seasonal"], "author": "Trim Gym Lawn Care", "status": "Published", "is_update": false } ] } }
Example payload — sync (user re-edited in Distribb, clicked “Sync to CMS”)
{ "event_type": "update_articles", "timestamp": "2026-04-19T09:12:33Z", "data": { "articles": [ { "id": "63452", "slug": "essential-lawn-maintenance-toronto-2026", "title": "Essential Lawn Maintenance Toronto Guide 2026 (Updated)", "content_html": "...revised body...", "status": "Published", "is_update": true, "update_only": true, "published_at": "2026-04-18T15:21:00Z" // remaining fields identical shape; only changed values differ } ] } }
Expected response (200)
{ "success": true, "action": "updated", "published_url": "https://yourdomain.com/blog/essential-lawn-maintenance-toronto-2026" }

Return the existing published_url so Distribb can show the article link. A 2xx response confirms receipt; it does not prove your public page has changed. Distribb checks the body for explicit failures, ignored updates, zero processed articles, and unexpected create responses. Initial publication can retry on server errors and timeouts; updates are sent once so an uncertain result is not replayed automatically.

Handling updates (recommended)

Users can click Send changes to Webhook on a published article or ask the Distribb Agent to send edits. Your existing route receives event_type: "update_articles", is_update: true, and update_only: true. Find the existing post by its stored Distribb ID, update its content, and preserve its permalink and original dates. Match by slug only for older posts without that ID. If no post matches, return 404.

if (payload.event_type === "update_articles") { // Placeholder DB methods; run inside your authenticated route. 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 }); return res.json({ success: true, action: 'updated', published_url: existing.url }); } else if (payload.event_type === 'publish_articles') { // Use a unique Distribb ID to deduplicate initial publish retries. await db.posts.upsertByDistribbId(article.id, mapArticleFields(article)); }

Never fall back to creating a post from an update event. If your route only handles publication today, add this update branch to the same route; you do not need another integration or webhook URL.

Common pitfalls
  • Wall-of-text output → you're rendering content_html through a Markdown parser (or content_markdown through an HTML renderer). Pick the field that matches your renderer.
  • Missing images → your CMS strips remote <img> tags. Either allow our S3 domain or re-upload image_url to your own storage before saving.
  • 401 from your endpoint → the bearer token in Settings doesn't match what your endpoint expects. Update one of them.
  • Duplicate posts after Sync → handle "update_articles" by finding and updating the existing post. Return an error if the post is missing.

MCP Server Model Context Protocol

Instead of writing curl commands, connect Distribb to Claude and call every endpoint as a native tool. The agent calls list_projects, create_article, publish_article directly, with no CLI flags and no shell scripts.

The hosted server is the recommended path: nothing to install, nothing to run, and you sign in with your Distribb account instead of pasting an API key.

1
Add the server URL

In Claude, open Settings → Connectors → Add custom connector and paste:

https://distribb.io/mcp
2
Approve access

You are redirected to Distribb, where you sign in and approve the connection. Distribb uses OAuth 2.0 with PKCE, so Claude never sees your password and you never paste an API key. Revoke access at any time from your Distribb settings.

3
Start working

All 18 tools appear immediately and run against your own projects only. Ask for what you want in plain language, for example “find keywords for my project and plan four articles for next month”.

Transport is streamable HTTP. Every tool enforces the same plan limits, rate limits and per-account scoping as the REST API documented above, because each tool call is dispatched to these very same endpoints.


Read-only tools (11)
list_projects List every project (website) on the account with plan, publishing cadence,...
get_project Full settings for one project: AI writing instructions, tone, publish sched...
list_articles List articles for a project or the whole account
get_article Fetch one article with its full HTML content, keyword, status, schedule, an...
backlinks_status Backlink exchange status for a project: credits, links received, links give...
list_backlinks List the verified backlinks a project has received and given through the Di...
search_console_performance Google Search Console performance for a project: clicks, impressions, posit...
ai_visibility_report How often the brand is cited by AI assistants (ChatGPT, Claude, Gemini, Per...
gbp_status Google Business Profile connection status and review stats for a project
gbp_reviews List live Google Business Profile reviews for a project
list_integrations List connected CMS and publishing integrations (WordPress, Webflow, Shopify...
Tools that write (7)

These change data. publish_article pushes a post to your connected CMS, and gbp_reply_review posts a reply that is publicly visible on Google. Nothing in this server deletes projects, articles or reviews.

update_project Update project settings: ai_instructions, tone, publish_time, timezone, bac...
create_project Create a new project for a website
create_article Plan or create an article on a project
update_article Update an article: title, content, keyword, meta_description, article_style...
publish_article Publish an article now to the connected CMS (WordPress, Webflow, Shopify, a...
keyword_research Run keyword research for a seed keyword: search volume, difficulty, and rel...
gbp_reply_review Post a public owner reply to a Google Business Profile review

Self-hosted alternative

Prefer to run it yourself, or using an editor without remote MCP support? A local stdio server is available in the distribb-skill repo. It authenticates with a DISTRIBB_API_KEY from your dashboard rather than OAuth, and exposes its own tool set.

Typical Workflow

Here is how an AI agent typically uses the API to create and publish an SEO article:

1. GET /projects # Get your project ID 2. GET /business-context?project_id=1 # Fetch brand voice + competitors 3. POST /keywords/search # Find target keywords 4. GET /internal-links?project_id=1&keyword=... # Get pages to link to 5. GET /backlink-targets?project_id=1&keyword=... # Get network URLs to cite 6. # Your AI writes the article using context from steps 2-5 7. POST /articles # Submit the article 8. POST /articles/123/publish # Publish to CMS

Errors

All error responses follow the same format:

{ "error": "Description of what went wrong" }
Status CodeMeaning
400Bad request — missing or invalid parameters
401Unauthorized — invalid or missing API key
402Payment required. project_limit_reached (POST /projects; body carries a purchase_url) or byo_keys_required (keyword search on a legacy Free Agentic account). Both return a machine-readable body with next-step instructions.
403Forbidden. Resource does not belong to your account, or a beta feature is closed
404Not found — resource does not exist
409Conflict. The target changed since it was staged (e.g. publishing a suggestion whose article moved)
429Rate limited. Too many requests, or a daily cap (e.g. AI-visibility scans) was hit; wait and retry
500Server error — something went wrong on our end