Open API Overview
The BlogShoot Open API lets automation partners (white-label sites, multi-tenant SaaS, agency tooling) drive the full lifecycle programmatically — create workspaces, configure onboarding, discover keywords, generate articles, and receive articles via webhook — without ever touching the web UI.
Base URL
Section titled “Base URL”https://api.blogshoot.comAll Open API routes live under the /open/ prefix.
Authentication
Section titled “Authentication”All Open API requests require a BlogShoot API key (bsk_* prefix).
Getting Your API Key
Section titled “Getting Your API Key”- Log in to app.blogshoot.com
- Go to Settings → API Keys
- Click “Create Key”
- Select scopes (see Scopes below)
- Copy the
bsk_*key once — it’s only shown once
Using API Keys
Section titled “Using API Keys”Two equivalent ways to authenticate:
# Option A: X-BlogShoot-Key header (recommended)curl -H "X-BlogShoot-Key: bsk_your_key" \ https://api.blogshoot.com/open/workspaces
# Option B: Bearer tokencurl -H "Authorization: Bearer bsk_your_key" \ https://api.blogshoot.com/open/workspacesOperating on a specific workspace
Section titled “Operating on a specific workspace”Most endpoints respect an optional X-Workspace-Id header that overrides the key owner’s default workspace for the request:
curl -H "X-BlogShoot-Key: bsk_..." \ -H "X-Workspace-Id: 6a21a0d88cdc5df997c026c6" \ https://api.blogshoot.com/open/articlesThis is how multi-tenant integrations route traffic to the correct customer’s workspace.
Scopes
Section titled “Scopes”Each API key is restricted to a set of scopes. Without the right scope, the endpoint returns 403 INSUFFICIENT_SCOPE.
| Scope | What it allows |
|---|---|
workspace:read | List + read workspace details |
workspace:write | Create, PATCH (partial-update), delete workspaces (each create/delete triggers Stripe quantity ±1) |
keywords:read | List discovered keywords |
keywords:write | Trigger keyword discovery |
articles:read | List + read articles + progress |
articles:write | Generate articles, retry failed generation |
planner:read | Read content plan |
planner:write | Refill calendar / trigger autopilot |
integrations:read | Read CMS connection state |
integrations:write | Push articles to WordPress / Shopify / webhook |
Select scopes when creating the key. Adding scopes later means rotating the key.
Available Endpoints
Section titled “Available Endpoints”Workspaces
Section titled “Workspaces”| Method | Path | Scope | Notes |
|---|---|---|---|
GET | /open/workspaces | workspace:read | List all workspaces owned by the key |
POST | /open/workspaces | workspace:write | Create workspace + Stripe quantity++ (charges $99/month per seat) |
PATCH | /open/workspaces/:workspace_id | workspace:write | Partial update — fill onboarding fields after creation |
DELETE | /open/workspaces/:workspace_id | workspace:write | Delete workspace + Stripe quantity— |
GET | /open/workspace/current | workspace:read | Read the key’s currently-selected workspace |
POST | /open/workspaces/:workspace_id/auto-fill-business | workspace:write | Trigger site analysis to crawl the workspace’s website and auto-extract business_info |
GET | /open/workspaces/:workspace_id/auto-fill-business | workspace:read | Check auto-fill result / status |
GET | /open/quotas | workspace:read | Account-wide quota snapshot — article credits + per-workspace per-scope discovery remaining |
POST /open/workspaces — Create
Section titled “POST /open/workspaces — Create”Creates a workspace immediately and bumps the Stripe subscription quantity by 1 (per-seat prorated charge). All onboarding fields are optional at create time; fill them later with PATCH.
curl -X POST https://api.blogshoot.com/open/workspaces \ -H "X-BlogShoot-Key: bsk_..." \ -H "Content-Type: application/json" \ -d '{ "website_url": "https://strand-example.com", "site_name": "Strand Example", "sitemap_url": "https://strand-example.com/sitemap.xml", "blog_base_path": "/article/", "business_info": { "company_name": "Strand", "industry": "Cross-border jewelry", "target_market": "United Arab Emirates", "content_language": "Arabic" }, "webhook_url": "https://your-cms.com/webhooks/blogshoot/strand", "webhook_secret": "whsec_..." }'Response:
{ "success": true, "workspace": { "id": "6a...", "website_url": "https://strand-example.com", "site_name": "Strand Example", "sitemap_url": "https://strand-example.com/sitemap.xml", "sitemap_validation": { "status": "pending", "blog_count": 0, "error": null }, "blog_base_path": "/article/", "business_info": { /* ... */ }, "business_info_auto_filled": true, "agent_config": { "team_instructions": "", "agents": {} } }, "subscription_quantity": 2, "webhook": { "id": "...", "webhook_url": "...", "webhook_secret": "whsec_..." }}Notes:
- If
business_infois provided at create time,business_info_auto_filledis set totrueso the BlogShoot UI shows this is programmatic, not user-typed. webhook_secretis returned once; store it server-side.
PATCH /open/workspaces/:workspace_id — Partial update onboarding fields
Section titled “PATCH /open/workspaces/:workspace_id — Partial update onboarding fields”Solves the typical automation flow: you create a workspace before the customer has finished onboarding (because you wanted to lock in pricing or grab a slot), then sync the onboarding data back later.
Body — all fields optional:
{ "site_name": "Strand UAE", "sitemap_url": "https://strand-example.com/sitemap-index.xml", "blog_base_path": "/ar/blog/", "example_blog_url": "https://strand-example.com/ar/blog/sample/", "business_info": { "company_name": "Strand", "industry": "Cross-border jewelry retail", "target_audience": "Middle Eastern women 25-45 interested in modern fine jewelry", "brand_voice": "Elegant, modern, with subtle cultural sensitivity", "key_products": "18k gold rings, diamond necklaces, custom bridal sets", "unique_selling_points": "Custom-made within 14 days; lifetime resizing", "target_market": "United Arab Emirates", "content_language": "Arabic", "company_story": "Founded in 2024 by ...", "product_catalog": [ { "name": "Eternity Ring 18k", "description": "...", "url": "https://..." } ], "service_areas": ["UAE", "Saudi Arabia", "Kuwait"] }, "agent_config": { "team_instructions": "Always write with cultural sensitivity for GCC audiences", "agents": { "content_writer_listicle": "Prefer numbered lists with 7-10 items..." } }}Merge strategies (via ?strategy=):
?strategy=merge(default) — Each field passed overrides the same field; fields not passed are preserved. Good for incremental sync.?strategy=replace—business_info/agent_configare reset to schema defaults plus your new fields. Use when you want to forcibly clear stale fields.
# Incremental: just bump the target_market without touching anything elsecurl -X PATCH "https://api.blogshoot.com/open/workspaces/6a.../" \ -H "X-BlogShoot-Key: bsk_..." \ -H "Content-Type: application/json" \ -d '{ "business_info": { "target_market": "Saudi Arabia" } }'
# Full reset of business_info to the exact object you supplycurl -X PATCH "https://api.blogshoot.com/open/workspaces/6a...?strategy=replace" \ -H "X-BlogShoot-Key: bsk_..." \ -H "Content-Type: application/json" \ -d '{ "business_info": { /* the whole new object */ } }'Response:
{ "success": true, "changed": true, "strategy": "merge", "changes": ["business_info", "sitemap_url"], "workspace": { /* full serialized workspace, same shape as create response */ }}Side effects:
- Changing
sitemap_urlresetssitemap_validation.status = "pending"andsitemap_auto_detected = false. The next validation pass (manual or scheduled) re-checks. - Any
business_infoPATCH setsbusiness_info_auto_filled = true.
DELETE /open/workspaces/:workspace_id
Section titled “DELETE /open/workspaces/:workspace_id”Deletes the workspace and decrements Stripe quantity by 1. Won’t decrement below 1 — to fully cancel the subscription, cancel via billing portal instead. Refuses if subscription_quantity already equals 1.
B2B vs B2C — Different keyword strategies
Section titled “B2B vs B2C — Different keyword strategies”workspace.business_info.business_model can be B2C (default — consumer-facing, optimize for traffic) or B2B (wholesale / OEM / supplier — optimize for inquiry generation). The discovery and ranking pipelines branch on this flag:
| Step | B2C flow | B2B flow |
|---|---|---|
| Seed extraction | Flat list of 5-10 broad seeds from business_info | 4 grouped sets along the buyer journey: Awareness / Comparison / Specification / Sourcing |
| Search-data expansion | 1 pass with all seeds | 4 passes (one per stage), each tagged with buyer_journey_stage |
| PAA expansion | Same (always runs) | Same (always runs); PAA-sourced keywords additionally tagged source: "paa" |
| Intent filtering | Standard intent ranking | B2B-specific scoring: +20 boost for buyer-action verbs (wholesale, OEM, MOQ, supplier, factory, quote, …), commercial/transactional intent ×3 weight, long-tail prioritized over head terms |
| Top-N target | 30 keywords | 40 keywords (more headroom across 4 stages) |
| Discovery budget | Low | Higher, because every buyer-journey stage is expanded separately |
Set the model at workspace creation or via PATCH:
curl -X PATCH "https://api.blogshoot.com/open/workspaces/:id" \ -H "X-BlogShoot-Key: bsk_..." \ -d '{"business_info":{"business_model":"B2B"}}'For B2B sites like wholesale jewelry suppliers, OEM factories, or cross-border trade platforms, this dramatically changes the keyword pool — instead of getting head terms like “best silver jewelry”, you get inquiry-driving keywords like “925 silver wholesale supplier Hangzhou MOQ 50”.
Buyer Journey stages — what they catch
Section titled “Buyer Journey stages — what they catch”Each keyword.buyer_journey_stage for B2B workspaces:
| Stage | Pattern | Example seeds | Inquiry value |
|---|---|---|---|
| Awareness | ”how to find”, “what is”, “MOQ meaning” | how to find reliable jewelry supplier | Educational — top-of-funnel |
| Comparison | ”X vs Y”, “best … for …” | Alibaba vs direct factory jewelry | Decision support — middle |
| Specification | technical/quality details | 925 silver vs gold plated durability | Quality verification — middle/bottom |
| Sourcing ⭐ | “wholesale … from …”, “OEM … factory” | wholesale jewelry from Hangzhou | Highest inquiry intent — bottom |
Filter to Sourcing-stage keywords when picking what to write articles about:
curl "https://api.blogshoot.com/open/keywords/list" \ -H "X-BlogShoot-Key: bsk_..." \ | jq '.keywords | map(select(.buyer_journey_stage == "Sourcing"))'Self-service: auto-fill business_info
Section titled “Self-service: auto-fill business_info”When you (as a partner) manage workspaces for end-customers, filling business_info manually is the bottleneck. Most customers fill only company_name + industry and skip the rest, which kills keyword quality.
The auto-fill endpoint solves this:
# Step 1: Trigger site analysis to crawl the customer's site and extract business_infocurl -X POST "https://api.blogshoot.com/open/workspaces/<workspace_id>/auto-fill-business" \ -H "X-BlogShoot-Key: bsk_..." \ -H "Content-Type: application/json" \ -d '{ "website_url": "https://strandjewelry.com" }'# Response: { "success": true, "job_id": "...", "message": "AI analysis started" }
# Step 2: Poll until done (~30-60 seconds)curl "https://api.blogshoot.com/open/workspaces/<workspace_id>/auto-fill-business" \ -H "X-BlogShoot-Key: bsk_..."# Response when complete:# {# "success": true,# "auto_filled": true,# "business_info": {# "company_name": "Strand Jewelry",# "industry": "Cross-border wholesale jewelry (China → US/EU/UK/MENA)",# "target_audience": "Overseas retailers and resellers...",# "key_products": "Silver, brass, gold-plated chains, earrings, statement pieces. Custom OEM available.",# "unique_selling_points": "Vetted Hangzhou workshops, one accountable contact, multilingual support, MOQ 12-100 pcs...",# ...# }# }
# Step 3: Show the result to your customer, let them review, then PATCH the final versioncurl -X PATCH "https://api.blogshoot.com/open/workspaces/<workspace_id>" \ -H "X-BlogShoot-Key: bsk_..." \ -H "Content-Type: application/json" \ -d '{ "business_info": { ...reviewed business_info... } }'Implementation notes:
- The crawler reads ~8 pages from the homepage and feeds them into the site-analysis pipeline
- Cost is charged against the account’s article credits (one call ≈ trivial compared to article generation)
- Unlike the web UI’s auto-fill (which has a “one-time per account” lockout), the Open API version can be called once per workspace — you can re-run it on the same workspace if the site significantly changed
business_info.business_modelis also auto-detected from on-site signals (wholesale terms, MOQ mentions, quote forms vs cart) — verify in the response, override via PATCH if wrong
Keywords
Section titled “Keywords”| Method | Path | Scope | Notes |
|---|---|---|---|
POST | /open/keywords/discover | keywords:write | Trigger discovery; accepts target_market + content_language for multilingual workspaces. B2B workspaces auto-run buyer-journey grouped expansion. |
GET | /open/keywords/list | keywords:read | List discovered keywords; supports ?language=, ?target_market=, ?buyer_journey_stage= filters |
See Multilingual integration for the multi-language flow.
POST /open/keywords/discover — Idempotency / merge semantics
Section titled “POST /open/keywords/discover — Idempotency / merge semantics”This endpoint is additive, not idempotent:
- The workspace has one
Keyworddocument. Calling discover again re-uses that document; it does not create a new one. - Each call appends new keywords to the existing
keywords[]array — old keywords (including already-used ones) are preserved. - Dedup key is
(lower(keyword), language). The same literal string can coexist as separate entries across languages (e.g. an English keyword and a Chinese keyword that happens to share characters both stay). - Each
discovery_historyentry is tagged withlanguage+target_market, so the weekly limit (5 / week / (workspace, language, target_market)) is independent per scope. Running 5 English discoveries does not lock out Arabic discoveries. - The response includes
remaining_this_week_for_scopeso you don’t need a second round-trip to read the limit state — schedule the next discovery accordingly.
# Day 1 — discover EN/UScurl -X POST https://api.blogshoot.com/open/keywords/discover \ -H "X-BlogShoot-Key: bsk_..." \ -d '{"target_market":"United States","content_language":"English"}'# Response: { keyword_id, remaining_this_week_for_scope: 4, ... }
# Day 2 — same scope, appends more keywords (deduped against day 1's keywords)curl -X POST https://api.blogshoot.com/open/keywords/discover \ -H "X-BlogShoot-Key: bsk_..." \ -d '{"target_market":"United States","content_language":"English"}'# Response: { keyword_id (same as day 1), remaining_this_week_for_scope: 3 }GET /open/keywords/list — Available fields per keyword
Section titled “GET /open/keywords/list — Available fields per keyword”Each item in keywords[] (and keywords_by_category[*][*]) exposes all the signals needed to pick top-N programmatically — no need to fetch a separate detail endpoint:
| Field | Type | Notes |
|---|---|---|
keyword | string | The keyword phrase |
language | string | Search language name (e.g. "English", "Chinese (Simplified)") |
target_market | string | Search market name (e.g. "United States", "China") |
search_volume | number | Estimated monthly searches |
keyword_difficulty | number | 0-100, lower = easier to rank |
cpc | number | Avg cost per click (USD) — proxy for commercial value |
competition | number | 0-1, paid competition signal |
relevance_score | number | 0-100, model-judged relevance to your business |
opportunity_score | number | ≈ volume × relevance / (difficulty + 1) — best single field to sort by. Not normalized — typical range is thousands to millions (use for relative ranking only, never compare against a fixed threshold) |
buyer_journey_stage | string | null | B2B workspaces only: Awareness / Comparison / Specification / Sourcing. null for B2C workspaces and for any keyword stored before the workspace was switched to B2B — re-run discovery to populate |
is_quick_win | bool | KD < 20 && volume > 100 shortcut |
search_intent | string | informational / commercial / transactional / navigational |
intent_category | string | Model-bucketed category (How-to & Guides, Best-of & Listicles, …) |
is_used | bool | True once an article has been generated from this keyword |
Suggested usage:
// Sort by combined opportunity, pick top 5 unusedconst picks = keywords .filter(k => !k.is_used) .sort((a, b) => b.opportunity_score - a.opportunity_score) .slice(0, 5);Articles
Section titled “Articles”| Method | Path | Scope | Notes |
|---|---|---|---|
POST | /open/articles/generate | articles:write | Generate article. keyword is the only required field — see below. |
GET | /open/articles | articles:read | List articles. Supports ?language= and ?target_market= filters. |
GET | /open/articles/:id | articles:read | Article detail |
GET | /open/articles/:id/progress | articles:read | Poll generation progress |
POST | /open/articles/:id/retry | articles:write | Retry failed generation |
POST | /open/articles/research-topic | articles:write | Pre-flight topic research |
POST /open/articles/generate — Request body
Section titled “POST /open/articles/generate — Request body”| Field | Required | Type | Notes |
|---|---|---|---|
keyword | yes | string | Target keyword. The only field a partner needs to pick — BlogShoot derives the rest. |
title | no | string | Initial placeholder title. Defaults to keyword. The pipeline’s SEO step rewrites article.title with an optimized headline before Ready, so this rarely matters in practice. |
type | no | string | One of Listicle / Step-by-Step Guide / Explainer. Defaults are derived from the keyword’s buyer_journey_stage: Sourcing / Comparison → Listicle; Awareness / Specification → Explainer; unknown → Explainer. |
language | no | string | Overrides the article’s language. Resolution order: this field → keyword’s language tag → workspace.business_info.content_language → "English". |
target_market | no | string | Same resolution chain as language. |
skip_overlap_check | no | bool | Escape hatch. By default, BlogShoot runs a semantic-overlap check against existing articles on the site; if a topic is >threshold similar to existing content, the call returns success:false, code:"TOPIC_OVERLAP" with the offending matches so you can decide. Set skip_overlap_check: true to force generation regardless (use when partner has already made the call). |
generation_params | no | object | Free-form params forwarded to the AI agents (e.g. custom best_article_url reference). |
Minimal partner request — just pick a keyword from /open/keywords/list and hand it over:
curl -X POST "https://api.blogshoot.com/open/articles/generate" \ -H "X-BlogShoot-Key: bsk_..." \ -H "X-Workspace-Id: <workspace_id>" \ -H "Content-Type: application/json" \ -d '{ "keyword": "wholesale silver jewelry MOQ 50" }'# Response: { "success": true, "article_id": "...", "status": "Draft" }The article goes through the async pipeline; subscribe to webhooks (see Article delivery) or poll GET /open/articles/:id/progress until status: "Ready".
Article delivery
Section titled “Article delivery”When an article reaches status: "Ready", BlogShoot auto-fires the webhook to every active connection on the workspace. You do not need to poll-and-push. See Webhook Integration.
Planner & CMS push (manual)
Section titled “Planner & CMS push (manual)”| Method | Path | Scope | Notes |
|---|---|---|---|
GET | /open/planner/content-plan | planner:read | |
POST | /open/planner/refill-calendar | planner:write | Smart Refill |
POST | /open/wordpress/article/push | integrations:write | Push article to a WordPress connection |
POST | /open/shopify/article/push | integrations:write | Push article to a Shopify connection |
POST | /open/webhook/article/push | integrations:write | Manually fire a webhook (auto-dispatch usually makes this unnecessary) |
Quotas
Section titled “Quotas”GET /open/quotas — Schedule-aware quota snapshot
Section titled “GET /open/quotas — Schedule-aware quota snapshot”Designed for partner-style integrations that drive BlogShoot from their own cron and need to schedule without “blind throwing”. Returns the full quota state in one call:
{ "success": true, "subscription": { "status": "active", "tier": "pro", "quantity": 2, "unit_price": 199, "total_monthly_price": 398, "next_billing_date": "2026-07-05T00:00:00Z" }, "article_credits": { "used": 80, "total": 400, "remaining": 320, "articles_used": 40, "articles_total": 200, "articles_remaining": 160, "period_started_at": "2026-06-05T00:00:00Z", "period_ends_at": "2026-07-05T00:00:00Z" }, "keyword_discovery": { "limit_per_week_per_scope": 5, "week_window": { "starts_at": "2026-05-31T00:00:00Z", "ends_at": "2026-06-07T00:00:00Z" }, "by_workspace": [ { "workspace_id": "6a...", "site_name": "Link4a", "website_url": "https://link4a.com", "autopilot_enabled": false, "by_scope": [ { "language": "English", "target_market": "United States", "used_this_week": 3, "remaining_this_week": 2 }, { "language": "Chinese (Simplified)", "target_market": "China", "used_this_week": 1, "remaining_this_week": 4 } ] } ] }}Notes:
article_creditsis account-level (all workspaces share one pool). One article costs 2 credits →articles_remaining = remaining / 2.keyword_discovery.by_workspace[*].by_scopeonly lists(language, target_market)combinations that have been used this week. Combinations not listed have full quota (5 / 5).autopilot_enabledreflects the workspace setting (defaulttrue). Whenfalse, BlogShoot’s internal AutoPilot will not auto-generate articles on this workspace — see the AutoPilot conflict protection section.
AutoPilot conflict protection
Section titled “AutoPilot conflict protection”If your integration drives article generation from your own cron, you must disable BlogShoot’s built-in AutoPilot on each workspace you manage, otherwise both pipelines will compete for the same keywords and credits.
curl -X PATCH "https://api.blogshoot.com/open/workspaces/<workspace_id>" \ -H "X-BlogShoot-Key: bsk_..." \ -H "Content-Type: application/json" \ -d '{ "autopilot_enabled": false }'Effects of autopilot_enabled: false:
- The daily AutoPilot cron skips this workspace (
autopilot_disabled_on_workspaceis logged) POST /open/planner/refill-calendarreturns{ "success": false, "code": "AUTOPILOT_DISABLED" }- All other endpoints (
/open/articles/generate,/open/keywords/discover, …) keep working — only internal automation is paused
Re-enable later (or never — partner-managed workspaces typically stay disabled forever):
curl -X PATCH "https://api.blogshoot.com/open/workspaces/<workspace_id>" \ -d '{ "autopilot_enabled": true }'Typical automation flow
Section titled “Typical automation flow”1. POST /open/workspaces → create + Stripe quantity++2. PATCH /open/workspaces/:id → fill onboarding incrementally3. PATCH /open/workspaces/:id → set { autopilot_enabled: false }4. POST /open/keywords/discover → with target_market + content_language response carries remaining_this_week_for_scope5. GET /open/keywords/list → pick top-N by opportunity_score6. GET /open/quotas (optional) → check article credits before bulk-scheduling7. POST /open/articles/generate → for each picked keyword8. (auto) → article reaches Ready → BlogShoot fires webhook9. (your side) → webhook receiver routes by article.languageErrors
Section titled “Errors”| HTTP | Error code | Meaning |
|---|---|---|
| 401 | MISSING_API_KEY | No X-BlogShoot-Key / Authorization: Bearer bsk_* header |
| 401 | INVALID_API_KEY | Key doesn’t exist or wrong format |
| 401 | API_KEY_REVOKED | Key was revoked |
| 403 | INSUFFICIENT_SCOPE | Key lacks the scope this endpoint requires |
| 402 | NO_ACTIVE_SUBSCRIPTION | Need an active Stripe subscription to call write endpoints |
| 402 | BILLING_FAILED | Stripe charge failed (e.g., card declined on workspace add) |
| 404 | WORKSPACE_NOT_FOUND | Workspace doesn’t belong to the key owner |
| 409 | QTY_CHANGE_IN_PROGRESS | Another mutation is currently running; retry shortly |
Always include the response body when reporting issues — error field identifies the failure precisely.
Next Steps
Section titled “Next Steps”- Webhook Integration — receive articles automatically
- Multilingual Integration — route by
article.language - WordPress Integration
- Astro / SSG Integration