Skip to content
Agile Snowglobe

Developer documentation · API v1

Your posts, your pipeline, over JSON.

Everything the dashboard does is available here: pull grounded, cited posts into any CMS, approve or reject them, steer the keyword map, schedule slots, read Search Console metrics and receive signed webhooks on every event.

Overview

The base URL is https://blog.fourdm.services/api/v1. Requests and responses are JSON; timestamps are ISO-8601; ids are integers. A key sees only the sites it belongs to: a site key sees one site, an account key sees every site on the account.

A typical integration does three things: pull status=published posts on a schedule or on a post.published webhook, render them, then call ack with the live URL so the canonical points at your page rather than the hosted copy.

Trying it out first? Write a free sample post, then create a key under Integrations for that site.

Authentication

Send your key as a bearer token on every request. Keys are shown once when created and only a hash is stored, so keep them in a secret store, never in a browser.

Authorization: Bearer sk_site_9_…
KeyFormatReachWhere
Site keysk_site_<id>_…One site. Scopes: posts read and write, sites read, suggestions, metrics, webhooks.Dashboard → site → Integrations → Create key
Account keysk_acct_…Every site on the account, with explicit scopes.Issued on request for agency accounts
Publishable keypk_<id>_…The embed script only; safe in page HTML.Site knowledge → Embed snippet

Scopes: posts:read, posts:write, sites:read, sites:write, keywords:write, suggestions:write, webhooks:write, metrics:read. A key without a required scope gets 403.

Conventions

  • Rate limit: 120 requests per minute per key. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; over the limit you get 429 with Retry-After.
  • Idempotency: creating POSTs (ack, generate, webhooks) accept an Idempotency-Key header. Repeating the same key within 24 hours returns the first response unchanged.
  • Pagination: the posts list is cursor based. Pass the returned next_cursor until has_more is false.
  • Caching: the posts list returns a weak ETag; send it back as If-None-Match to get 304 when nothing changed.
  • Long work: generation and re-indexing return 202 with a job_id; poll GET /jobs/{id}.

Posts

The post object

One shape everywhere: every posts endpoint and every post.* webhook payload returns it.

FieldTypeMeaning
id, site_idintPost and site identifiers.
statusstringdraft, pending_review, scheduled, published, failed.
slug, title, h1stringURL slug and headings.
meta_title, meta_description, excerptstringSEO meta, trimmed at word boundaries; excerpt is the first 170 characters of the description.
content_html, content_markdownstringThe body with table of contents, key-takeaways box, FAQ and internal links. Render the HTML, or the Markdown if your CMS prefers it.
word_countintBody length.
primary_keyword, intentstringThe search the post targets and its intent.
category_id, author_idint | nullYour category and byline author, when set.
schema_jsonldstring | nullArticle and FAQPage JSON-LD, ready to place in a <script type="application/ld+json">.
banner_urlstring | nullThe generated 1680×882 banner (a .webp sits beside every .jpg).
canonical_urlstring | nullSet by ack; points at your live page.
scores, publishableobject, boolThe editor model's review: overall plus issues; publishable is the gate's verdict.
published_at, approved_at, refreshed_at, created_at, updated_atdatetime | nullISO-8601, UTC.

List posts

GET/sites/{site_id}/postsposts:read
QueryTypeMeaning
statusstringFilter by status, usually published.
updated_sinceISO-8601Only posts changed since this time; ideal for a sync job.
cursorintThe next_cursor from the previous page.
limitint1 to 100, default 50.
curl "https://blog.fourdm.services/api/v1/sites/9/posts?status=published&limit=2" \
  -H "Authorization: Bearer $SNOWGLOBE_KEY"

Get a post

GET/posts/{post_id}posts:read

The full post object. 404 when the post belongs to a site the key cannot see.

curl "https://blog.fourdm.services/api/v1/posts/23" \
  -H "Authorization: Bearer $SNOWGLOBE_KEY"

Acknowledge delivery

POST/posts/{post_id}/ackposts:write · Idempotency-Key

Tell us the post is live on your site. The remote_url becomes the canonical URL (unless the post has been unpublished), and a post.published event is emitted with the updated post.

curl -X POST "https://blog.fourdm.services/api/v1/posts/23/ack" \
  -H "Authorization: Bearer $SNOWGLOBE_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: deploy-2026-09-24-23" \
  -d '{"remote_url": "https://seodada.com/blog/technical-seo-audit-checklist"}'

Approve, reject, refresh, unpublish

CallBodyEffect
POST /posts/{id}/approve—Approves a post waiting in approve-first mode; it publishes at its slot time.
POST /posts/{id}/reject{"reason"}Rejects it; the slot regenerates. 409 already_published if it already went live.
POST /posts/{id}/refresh—Queues a refresh: weak sections are rewritten and the post republishes at the same URL.
DELETE /posts/{id}—Unpublishes: status back to draft, the hosted copy hidden, the keyword released. Emits post.unpublished.
curl -X POST "https://blog.fourdm.services/api/v1/posts/23/reject" \
  -H "Authorization: Bearer $SNOWGLOBE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Wrong product name in section 3"}'

Automation

GET/sites/{site_id}/automationsites:read
PATCH/sites/{site_id}/automationsites:write
FieldTypeMeaning
modestringfull_auto, approve_first or draft_only. Full auto stays locked until the site's 30-day quality average clears the bar (trust_state).
timezone, publish_timeIANA, HH:MMWhen posts publish, in the site's local time.
weekdaysint[]0 = Monday … 6 = Sunday.
targetsstring[]Any of pull, wordpress, webhook.
pausedboolStops generation without losing settings.
posts_per_week, auto_refresh, auto_approve_after_hours, cadence_targetread-only hereSet from the dashboard.
curl -X PATCH "https://blog.fourdm.services/api/v1/sites/9/automation" \
  -H "Authorization: Bearer $SNOWGLOBE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mode": "approve_first", "publish_time": "06:00", "weekdays": [0, 2, 4], "targets": ["wordpress", "webhook"]}'

Slots

CallBody or queryEffect
GET /sites/{id}/slots?from=&to=The calendar: one slot per scheduled day with date, status, post_id, publish_at.
POST /sites/{id}/slots{"date", "cluster_id?"}Adds a slot on that date at the site's publish time; 409 slot_exists if the day is taken.
DELETE /slots/{slot_id}—Skips a scheduled slot.

Generate now and jobs

POST/sites/{site_id}/generateposts:write · Idempotency-Key · 202

Queues one post outside the schedule. Pass a keyword to choose the topic; leave it out and the keyword map picks the next one. Poll the job until status is done and read post_id.

curl -X POST "https://blog.fourdm.services/api/v1/sites/9/generate" \
  -H "Authorization: Bearer $SNOWGLOBE_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cwv-2026-09-24" \
  -d '{"keyword": "core web vitals for content sites"}'
GET/jobs/{job_id}
{ "id": "184", "kind": "generate", "status": "done", "site_id": 9, "post_id": 25, "error": null }

Sites and keywords

CallBody or queryEffect
GET /sites—The sites this key can see: id, name, url, language, description, house_style, active.
GET /sites/{id}—One site.
PATCH /sites/{id}{"name", "description", "house_style", "language"}Voice and language changes apply to the next post.
POST /sites/{id}/reindex—Re-crawls the site into the knowledge index. 202 with a job id.
GET /sites/{id}/clusters?status=The keyword map: primary, secondaries, intent, format, priority, status, money_url.
POST /clusters/{id}/approve · /ban—Plan a cluster next, or keep it out. Scope keywords:write.
POST /sites/{id}/seeds{"keywords": [...]}Adds seed keywords the next backlog refresh expands.
curl -X POST "https://blog.fourdm.services/api/v1/sites/9/seeds" \
  -H "Authorization: Bearer $SNOWGLOBE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"keywords": ["log file analysis", "hreflang for multi-language sites"]}'

Suggestions and metrics

CallQueryEffect
GET /sites/{id}/suggestions?status=openThe Brain's advice for the site: type, title, detail, impact, effort, priority, post_id.
POST /suggestions/{id}/accept · /dismiss—Accepting a refresh suggestion queues the refresh. Scope suggestions:write.
GET /sites/{id}/metrics?from=&to=Search Console rows per post and period: clicks, impressions, ctr, position. Scope metrics:read.
curl "https://blog.fourdm.services/api/v1/sites/9/metrics?from=2026-09-01&to=2026-09-30" \
  -H "Authorization: Bearer $SNOWGLOBE_KEY"

Webhooks

Register a URL and we POST a signed JSON envelope for the events you choose. Failed deliveries retry at 1, 5, 30, 120, 360 and 1440 minutes; every attempt is visible in the delivery log and can be replayed from the dashboard or the API.

CallBodyEffect
GET /webhooks—Your webhooks.
POST /webhooks{"url", "events?", "site_id?"}201 with the webhook and its secret, shown once. No events means all of them.
DELETE /webhooks/{id}—Removes it.
GET /webhooks/{id}/deliveries—Last 100 deliveries: event_type, status, attempts, response_code, last_error, next_attempt_at.
POST /webhooks/{id}/deliveries/{did}/replay—Sends that delivery again now.
curl -X POST "https://blog.fourdm.services/api/v1/webhooks" \
  -H "Authorization: Bearer $SNOWGLOBE_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: hook-seodada-1" \
  -d '{"url": "https://hooks.seodada.com/snowglobe", "events": ["post.published", "post.updated"], "site_id": 9}'

Events: post.generated, post.pending_review, post.approved, post.published, post.updated, post.failed, post.unpublished, slot.skipped, site.indexed, suggestion.created, budget.threshold, connection.failed.

Envelope:

{
  "id": "evt_9f2c1a7e4b3d",
  "type": "post.published",
  "created_at": "2026-09-24T00:30:12Z",
  "data": { "post": { "id": 23, "site_id": 9, "status": "published", "…": "…" } }
}

Verifying signatures

Every delivery carries X-Snowglobe-Signature: t=<unix>,v1=<hex>, where v1 is HMAC-SHA256 of "{t}.{raw body}" with your webhook secret. Reject anything older than five minutes and compare in constant time.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(header, secret, rawBody) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

WordPress and WooCommerce plugin

If your site runs WordPress, you do not need to write against the API at all. The plugin receives each post the moment it is published through a signed webhook it registers itself, with a pull every 15 minutes as a safety net. It creates the post once and updates the same post on later edits, refreshes or a new banner, copies the banner and the infographics into your media library, sets the meta title and description for Yoast SEO and Rank Math, prints the Article and FAQPage JSON-LD, and acknowledges the live URL so the canonical points at your site. On a WooCommerce store it also sends your products to Snowglobe, so posts quote today's prices and link the right product.

Download the plugin (zip)

Install and connect

  1. Plugins → Add New → Upload Plugin, choose the zip, install and activate.
  2. In the dashboard open the site → Integrations → WooCommerce and WordPress plugin → Create connection code. The code holds the API address, the site id and a new site key; it is shown once.
  3. In WordPress open Settings → Agile Snowglobe, paste the code and press Connect. The plugin registers its webhook, pulls every published post and, with WooCommerce, uploads the catalogue 100 products at a time.
  4. Keep the dashboard's WordPress connector (the application-password one) disconnected, otherwise the post is published twice. The Integrations page warns you when both are on.

The Integrations page shows the plugin's WordPress and WooCommerce versions, the products in the site's knowledge, the last webhook delivery and the last check-in, and offers an update when a newer plugin is out.

What it does with each post

Post fieldWordPress
title, slug, content_html_with_imagesPost title, slug and content with the infographics placed after their sections; the leading H1 is dropped because your theme renders the title.
excerpt / meta_descriptionPost excerpt, plus _yoast_wpseo_title, _yoast_wpseo_metadesc, rank_math_title, rank_math_description.
banner_url_absolute, infographic_urlsCopied into the media library once with alt text; the banner becomes the featured image.
schema_jsonldStored on the post and printed in the head of its page.
published_at, updated_atThe post date on first import; unchanged posts are skipped on later pulls.
internal linksLinks that resolve to WooCommerce products are remembered; the [snowglobe_products] shortcode renders them as a product grid (appended automatically when the option is on).
post.unpublishedThe WordPress post goes back to draft.

Product knowledge (WooCommerce)

Published, visible products are sent as structured data: name, price and sale price, the price range of variable products, currency, stock, SKU, categories, tags, visible attributes and descriptions. Snowglobe stores each as a trusted product page of the site; while the plugin is connected a crawl never overwrites it. Saving a product sends it a minute later in the background, and trashing or unpublishing it removes it. Nothing about customers or orders leaves the store. You can call the same endpoints from any other shop system with a site key.

CallBody and result
POST /sites/{id}/plugin/connect{"webhook_url", "site_url", "plugin_version", "wp_version", "woocommerce_version"}. Registers the plugin's webhook for post.published, post.updated and post.unpublished, replacing an earlier one for the same URL, and returns webhook_secret once. Scope webhooks:write.
POST /sites/{id}/plugin/heartbeatVersions plus synced, errors, last_result. Shown on the Integrations page. Scope sites:read.
POST /sites/{id}/products{"products": [{"url", "name", "price", "max_price", "regular_price", "sale_price", "currency", "sku", "stock_status", "short_description", "description", "categories", "tags", "attributes", "image"}]}, up to 50 per call; the URL must be on the site's domain or a subdomain. Returns upserted, unchanged, skipped with reasons, and the product count. Scope knowledge:write.
POST /sites/{id}/products/remove{"urls": [...]}. Removes those pages from the site's knowledge. Scope knowledge:write.

Keys created before the knowledge:write scope existed get a 403 on the product calls; create a new connection code to replace the key. The plugin is plain PHP with no build step: the bootstrap, an API client, the connect flow, the post sync, the product sync, the webhook receiver and the settings page. Read it before you install it; it is yours to change.

Legacy pull API

The original read-only API stays available for sites that integrated before v1. It uses the site's older per-site key as an X-API-Key header (or ?api_key=) and serves published posts only.

CallReturns
GET /api/postsPublished posts, newest first, with limit and offset.
GET /api/posts/{slug}One published post by slug.
GET /api/relatedPosts related to a slug by embedding similarity.

New integrations should use v1: it adds approval, scheduling, keywords, metrics and webhooks, and its post object carries schema, images and scores.

Errors and limits

Validation and state errors return an error object; authentication and rate-limit errors return a plain detail string.

{ "error": { "code": "slot_exists", "message": "A post is already scheduled for 2026-09-30.", "field": "date" } }
{ "detail": "missing scope: posts:write" }
StatusMeaningTypical cause
400Bad requestbad_param, bad_date, bad_timezone, bad_url, bad_action.
401UnauthorisedNo bearer token, or a revoked or unknown key.
403ForbiddenThe key lacks the scope, or is scoped to another site.
404Not foundThe site, post, slot, cluster, suggestion or webhook is not visible to this key.
409Conflictslot_exists, already_published.
429Too many requestsMore than 120 calls in a minute; wait Retry-After seconds.

OpenAPI

The machine-readable spec is at https://blog.fourdm.services/api/v1/openapi.json, and an interactive explorer at https://blog.fourdm.services/api/docs. Import the JSON into Postman or Insomnia, or generate a client with openapi-generator.