API Reference · v1

External Publishing API

Programmatically publish articles into any site in your GASEO project from external tools — Zapier, N8N, custom Python scripts, in-house writer pipelines.

Base URL
https://gaseo.ai/api/v1
Auth
Bearer API key
Format
JSON

Quick Start

Three steps from zero to a published article. You should be done within five minutes.

1

Get an API key

In the GASEO dashboard, open your project → API Keys (left toolbox) → + New API Key. Copy the full token the moment it appears — you cannot see it again.

2

List the sites you can publish to

curl https://gaseo.ai/api/v1/projects/{projectId}/sites \
  -H "Authorization: Bearer ak_live_..."

The response includes a sites[] array — each item has a siteId you'll use in step 3.

3

Publish an article

curl -X POST https://gaseo.ai/api/v1/sites/{siteId}/articles \
  -H "Authorization: Bearer ak_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "title": "My first API article",
    "content": "## Hello\n\nMarkdown body here.",
    "status": "published"
  }'

You'll get back an article.url — the live page. Done.

Authentication

Every request needs an Authorization: Bearer <token> header. Tokens are minted in the dashboard and look like ak_live_3dd2104b9144794e76ff437f655784bb (project-scoped) or ak_user_… (Personal Access Token, can access all your projects).

The full token is shown exactly once — at creation. After that the dashboard only shows the 12-character prefix. Store the full token in a password manager or secrets vault.

Key scope

  • Project-scoped (ak_live_): bound to one project. Can read/write only that project.
  • Personal Access Token (ak_user_): bound to your account. Can access every project you own or are a member of.
  • Neither key can touch other tenants' projects or modify dashboard settings.

Revoking a key

Open the API Keys panel and click Revoke. Effective immediately — subsequent requests get 401 invalid_key.

GET List sites in a project

GET https://gaseo.ai/api/v1/projects/{projectId}/sites

Discovery endpoint. Lists every site in the project so you can pick a siteId to publish to.

Path parameters

FieldTypeNotes
projectIdstringThe project ID bound to your API key. Visible in the dashboard URL.

Response

{
  "success": true,
  "projectId": "proj_abc123",
  "projectName": "My Casino Project",
  "sites": [
    {
      "siteId": "site_111",
      "name": "Casino Royale",
      "url": "https://casino-royale.gaseo.ai",
      "primaryDomain": "casino-royale.com",
      "testDomain": "casino-royale",
      "language": "en",
      "tags": ["casino", "review"],
      "status": "completed",
      "articleCount": 12
    }
  ]
}

Response fields

FieldTypeNotes
sites[].siteIdstringUse this in the POST endpoint URL.
sites[].urlstringThe runtime URL — every site has one.
sites[].primaryDomainstring | nullVerified custom domain, if any.
sites[].languagestring | nullBCP-47 language tag of the site's content.
sites[].tagsstring[]Free-form site tags for batch routing.
sites[].statusstringGeneration status: generating / completed / failed.
sites[].articleCountnumberCount of draft+published articles.

POST Create an article

POST https://gaseo.ai/api/v1/sites/{siteId}/articles

Publishes (or schedules, or saves as draft) a single article. Returns the article ID, slug, and live URL.

Request body

FieldTypeRequiredNotes
titlestringyesArticle title. ≤ 300 chars.
contentstringyesMarkdown body. ≤ 200,000 chars. Inline <script>, event handlers, and javascript: URLs are stripped on ingest.
statusenumyesOne of draft · published · scheduled. See status semantics.
slugstring—Custom URL slug. Defaults to slugified title. Auto-suffixed on collision; the actual slug used is returned in the response.
excerptstring—Listing summary. ≤ 500 chars. Auto-derived from content if absent.
metaDescriptionstring—HTML <meta description>. ≤ 200 chars.
targetKeywordstring—SEO target. ≤ 120 chars. Used internally to avoid keyword cannibalisation.
tagsstring[]—≤ 20 entries. Free-form.
featuredImage.urlstring—Public URL to an image (JPEG / PNG / WebP / GIF / AVIF, ≤ 10 MB). SVG is rejected.
featuredImage.altstring—Alt text. ≤ 200 chars.
featuredImage.rehostboolean—Default true. See image rehosting.
inlineImagesarray—≤ 20 entries of { url, alt?, position? }. Each image is rehosted to our CDN and woven into the markdown body — see the Images section.
ctaUrlstring—CTA target. Falls back to site's default.
publishedAtISO 8601conditionalRequired when status=scheduled; optional otherwise.
externalIdstring—Echoed in the response. Use for your own dedup / tracking.

Response (201 Created)

{
  "success": true,
  "article": {
    "articleId": "65fc...",
    "siteId": "site_111",
    "title": "Top 10 Slot Games of 2026",
    "slug": "top-10-slot-games-2026",
    "status": "published",
    "url": "https://casino-royale.gaseo.ai/blog/top-10-slot-games-2026",
    "publicUrl": "https://casino-royale.com/blog/top-10-slot-games-2026",
    "publishedAt": "2026-04-29T10:00:00.000Z",
    "scheduledPublishAt": null,
    "createdAt": "2026-04-29T10:00:01.234Z",
    "externalId": "client-uuid-12345",
    "featuredImage": {
      "url": "https://cdn.gaseo.ai/articles/site_111/.../hero.jpg",
      "alt": "Slot reels spinning",
      "rehosted": true
    }
  }
}

PATCH Update an article

PATCH https://gaseo.ai/api/v1/sites/{siteId}/articles/{articleId}

Edits one or more fields. Only fields present in the body are touched — omitted fields keep their current values. The full updated article is returned.

Editable fields

Same shape and rules as create. Common fields: title, content, slug, excerpt, metaDescription, targetKeyword, tags, ctaUrl, status (+ publishedAt), featuredImage.

Common patterns

# Publish an existing draft
PATCH .../articles/65fc...
{ "status": "published" }

# Reschedule a scheduled post
PATCH .../articles/65fc...
{ "status": "scheduled", "publishedAt": "2026-05-15T08:00:00Z" }

# Replace featured image (rehosted to CDN; response includes featuredImageRehost)
PATCH .../articles/65fc...
{ "featuredImage": { "url": "https://...", "alt": "New hero" } }

# Weave images into the body (rehosted to CDN)
PATCH .../articles/65fc...
{ "inlineImages": [ { "url": "https://...", "alt": "Lobby", "position": 2 } ] }

# Change slug (auto-suffixed on collision)
PATCH .../articles/65fc...
{ "slug": "better-slug" }

Notes

  • Changing slug follows the same auto-suffix-on-collision rule as POST.
  • Status transitions follow the same semantics as create.
  • You cannot move an article between sites by changing siteId — it's path-locked.
  • When you replace featuredImage, the response includes a featuredImageRehost object (rehosted, passthrough?, error?, errorCode?) so you can verify the permanent CDN URL took.

GET List articles on a site

GET https://gaseo.ai/api/v1/sites/{siteId}/articles

Returns a summary list of articles. Use this to see what you've already published and to find article IDs for the GET / PATCH endpoints.

Query parameters

ParamTypeDefaultNotes
statusenum—Filter by draft · published · scheduled · archived. Omit to list all.
limitnumber50Page size. Max 100.
offsetnumber0Pagination offset. Sorted by createdAt desc.

Response (200)

{
  "success": true,
  "siteId": "site_111",
  "total": 42,
  "limit": 50,
  "offset": 0,
  "articles": [
    {
      "articleId": "65fc...",
      "siteId": "site_111",
      "title": "Top 10 Slots",
      "slug": "top-10-slots",
      "status": "published",
      "excerpt": "Short summary...",
      "publishedAt": "2026-04-29T10:00:00.000Z",
      "scheduledPublishAt": null,
      "createdAt": "2026-04-29T10:00:00.000Z",
      "updatedAt": "2026-04-29T10:00:00.000Z",
      "url": "https://casino-royale.gaseo.ai/blog/top-10-slots",
      "publicUrl": "https://casino-royale.com/blog/top-10-slots",
      "tags": ["slot", "review"]
    }
  ]
}

The summary list does NOT include the full markdown body — use GET single article to fetch content.

GET Get one article

GET https://gaseo.ai/api/v1/sites/{siteId}/articles/{articleId}

Returns the full article including the markdown content, meta description, featured image, and tags.

Response (200)

{
  "success": true,
  "article": {
    "articleId": "65fc...",
    "siteId": "site_111",
    "title": "Top 10 Slots",
    "slug": "top-10-slots",
    "status": "published",
    "content": "## Intro\n\nFull markdown body here.",
    "excerpt": "Short summary...",
    "metaDescription": "SEO meta description ≤ 200 chars",
    "targetKeyword": "best slot games 2026",
    "ctaUrl": "https://play.example.com/?ref=xyz",
    "publishedAt": "2026-04-29T10:00:00.000Z",
    "scheduledPublishAt": null,
    "createdAt": "2026-04-29T10:00:00.000Z",
    "updatedAt": "2026-04-29T10:00:00.000Z",
    "url": "https://casino-royale.gaseo.ai/blog/top-10-slots",
    "publicUrl": "https://casino-royale.com/blog/top-10-slots",
    "tags": ["slot", "review"],
    "featuredImage": {
      "url": "https://cdn.gaseo.ai/articles/.../hero.jpg",
      "alt": "Slot reels"
    }
  }
}

POST Upload image

POST https://gaseo.ai/api/v1/sites/{siteId}/images

Upload an image and get back a permanent CDN URL. Use it as featuredImage.url, in inlineImages, or directly inside markdown — article create/update recognises our CDN URLs and uses them as-is (no refetch). This is the right tool when the image source is WAF / hotlink protected and a server-side fetch from us would get 403.

Three input modes

  • multipart/form-data with a file field — binary upload, preferred.
  • JSON { "base64": … } — raw base64 or a data: URI.
  • JSON { "url": … } — we fetch the source server-side and rehost in one call. Fails with a classified errorCode instead of silently falling back.
# a. multipart (binary upload — preferred for WAF-blocked sources)
curl -X POST .../sites/site_111/images \
  -H "Authorization: Bearer ak_live_..." \
  -F "file=@./hero.jpg"

# b. JSON base64 (data: URI prefix tolerated)
{ "base64": "/9j/4AAQSkZJRg..." }

# c. JSON url (server-side fetch + rehost in one call)
{ "url": "https://images.pexels.com/photos/12345.jpg" }

Response (201)

{
  "success": true,
  "image": {
    "url": "https://cdn.gaseo.ai/articles/site_111/uploads/1765432800-b7f2d1.jpg",
    "key": "articles/site_111/uploads/1765432800-b7f2d1.jpg",
    "contentType": "image/jpeg",
    "bytes": 482133,
    "source": "file"
  }
}

Notes

  • Allowed formats: jpeg, png, webp, gif, avif — detected from the actual bytes; declared filenames / content types are not trusted. SVG is rejected. Max 10 MB.
  • Counts against the write rate-limit bucket.
  • Uploaded files are not tied to one article — reuse the URL across as many articles as you like. They are never deleted by DELETE article.

DELETE Delete article

DELETE https://gaseo.ai/api/v1/sites/{siteId}/articles/{articleId}

Permanently removes the article — no undo. Built for cleaning up failed or test drafts so they don't pile up.

Response (200)

{
  "success": true,
  "deleted": true,
  "articleId": "65fc...",
  "siteId": "site_111",
  "imagesDeleted": 3
}

Notes

  • Per-article CDN images (rehosted under the article's own folder) are deleted best-effort alongside it.
  • Standalone uploads from the image upload endpoint are kept — they may be shared by other articles.
  • To hide an article without destroying it, PATCH status to "draft" instead.

Status semantics

The status field is required and accepts three values. The combination of status and publishedAt determines when the article becomes publicly visible.

status + publishedAtStored asWhen visible
"draft"draftNever (until you flip it via dashboard).
"published" (no publishedAt)publishedImmediately. publishedAt = now.
"published" + past publishedAtpublishedImmediately, with the historical date for backfills.
"published" + future publishedAtdraft (auto-promoted to scheduled)At the supplied publishedAt.
"scheduled" + future publishedAtdraftAt the supplied publishedAt (worker flips it live).
"scheduled" + missing or past publishedAt400 validation_failed

Image rehosting

When you supply featuredImage.url with rehost: true (the default), we fetch the source URL and re-upload the bytes to our CDN. The response returns the new permanent URL.

Why rehost?

  • Your source URL might disappear (Pexels removes images, user deletes the file, etc.).
  • Some sources block hot-linking with referer checks.
  • CDN-served images are faster and respect Core Web Vitals.

What if rehost fails?

If the source returns non-2xx, has the wrong content type, or exceeds 10 MB, we fall back to keeping the original URL and set featuredImage.rehosted: false with an error message plus a machine-readable errorCode (see the failure codes table below). The article is still created. If the source is WAF-protected (fetch_blocked), upload the bytes directly via POST /sites/{siteId}/images instead.

Skip rehosting

Set featuredImage.rehost: false if you've already hosted the image on a CDN you trust.

Accepted formats

image/jpeg · image/png · image/webp · image/gif · image/avif. Max 10 MB. SVG is rejected — it can carry scripts and would XSS anything that renders it inline.

CDN URL passthrough

URLs that already live on our CDN (returned by the image upload endpoint) are used as-is — no refetch, no duplicate copy — and marked passthrough: true. Pre-uploading images makes article create/update fast and immune to source-side WAF blocks.

Rehost failure codes

Wherever we fetch an image URL on your behalf (featured image, inline images, upload-by-url), failures carry a machine-readable errorCode so you know whether to retry or fall back to a direct upload:

errorCodeWhen
fetch_blockedSource returned 401/403 — WAF / hotlink protection. Upload the bytes directly via POST /sites/{siteId}/images instead.
fetch_rate_limitedSource returned 429. Back off and retry later.
fetch_not_foundSource returned 404/410. Fix the URL.
fetch_timeoutNo response within 15 s. Retry; if persistent, upload directly.
fetch_dns_errorHostname did not resolve.
fetch_ssrf_blockedURL points at a private/internal address. Never allowed.
unsupported_content_typeNot a jpeg/png/webp/gif/avif.
image_too_largeOver 10 MB. Compress or resize first.
r2_upload_failedOur storage hiccuped. Safe to retry.

Inline images

Pass inlineImages: [{ url, alt?, position? }] (max 20) on create or update to have body images rehosted to our CDN and woven into the markdown. Per image: if content already references the url, every occurrence is replaced in-place with the CDN url; otherwise it is inserted after paragraph index position (0 = top), or appended at the end. Each image reports placement, rehosted, and errorCode in the response. Plain markdown images not listed in inlineImages are stored as-is and keep pointing at their source URLs.

Errors

All errors share this envelope:

{
  "success": false,
  "error": {
    "code": "validation_failed",
    "message": "status is required (string)",
    "field": "status"
  }
}
HTTPcodeWhen
400validation_failedMissing or invalid field. field names the bad one.
401invalid_authorizationMissing or malformed Authorization header.
401invalid_keyToken not recognised, revoked, or expired.
403forbidden_projectToken is valid but for a different project.
403forbidden_siteThe site exists but belongs to another project.
404project_not_foundNo project with this projectId.
404site_not_foundNo site with this siteId.
400image_invalidUpload payload is not a valid jpeg/png/webp/gif/avif, or exceeds 10 MB.
422image_fetch_failedUpload-by-url source could not be fetched. Includes errorCode + sourceUrl.
429rate_limit_exceededRate limit exceeded. Includes retryAfterSeconds and a Retry-After header.
500internal_errorSomething blew up on our side. Safe to retry.

Rate limits

Limits are per API key in fixed 60-second windows: 600 requests/min for reads (GET) and 120 requests/min for writes (POST / PATCH / DELETE, including image uploads). Every response carries the current state:

X-RateLimit-Limit:     120        # requests allowed per 60s window
X-RateLimit-Remaining: 87         # requests left in the current window
X-RateLimit-Reset:     1765432800 # unix seconds when the window resets

# on 429:
Retry-After: 42                   # seconds to wait before retrying

On 429, honor Retry-After — don't blind-retry. Batch clients should watch X-RateLimit-Remaining and pace themselves.

Code examples

Node.js (fetch)

const res = await fetch('https://gaseo.ai/api/v1/sites/site_111/articles', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.GASEO_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    title: 'My Article',
    content: '## Hello\n\nWorld',
    status: 'published'
  })
});
const data = await res.json();
if (!data.success) throw new Error(data.error.message);
console.log(`Published: ${data.article.url}`);

Python (requests)

import os, requests

res = requests.post(
    'https://gaseo.ai/api/v1/sites/site_111/articles',
    headers={'Authorization': f"Bearer {os.environ['GASEO_API_KEY']}"},
    json={
        'title': 'My Article',
        'content': '## Hello\n\nWorld',
        'status': 'published',
    },
    timeout=30,
)
res.raise_for_status()
data = res.json()
print('Published:', data['article']['url'])

N8N (HTTP Request node)

  1. Add an HTTP Request node.
  2. Method: POST
  3. URL: https://gaseo.ai/api/v1/sites/{{ $json.siteId }}/articles
  4. Authentication: Header Auth → name Authorization → value Bearer ak_live_...
  5. Body Content Type: JSON, then drop in the fields from the create endpoint.

Zapier (Webhooks by Zapier)

  1. Action: Webhooks by Zapier → Custom Request.
  2. Method: POST
  3. URL: the full /sites/{siteId}/articles endpoint.
  4. Headers: Authorization: Bearer ak_live_... and Content-Type: application/json.
  5. Data: the JSON body. Map fields from previous steps.

MCP · Connect via AI assistants

Skip the curl entirely. Plug GASEO into Claude Desktop, Cursor, or Cline and ask the AI to write & publish articles for you.

We ship a Model Context Protocol server as the gaseo-mcp npm package. Once configured, your AI assistant gets 8 tools — list_projects, list_sites, list_articles, get_article, create_article, update_article, upload_image, delete_article — that wrap this API.

What it looks like

You: "List my GASEO sites then publish a 500-word article about why MMORPGs are due a comeback to the first one."

Claude: (calls list_sites → drafts markdown → calls create_article with status=published) → "Done. Live at https://..."

Easiest setup — let your AI do it

If you already use an AI assistant with file access (Claude Desktop, Cursor, Cline, ChatGPT with code interpreter, …) the fastest path is to ask it to install gaseo-mcp for you. The dashboard's "+ New API Key" flow shows an "AI install prompt" tab — copy that, paste into your AI chat, and the AI handles the config file edits (correct path per OS, JSON merge, backup, etc.).

Works equally well for first-time install and for token rotation — the prompt tells the AI to replace any existing gaseo entry. No need to know where your MCP config lives.

Manual setup — Claude Desktop

Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):

{
  "mcpServers": {
    "gaseo": {
      "command": "npx",
      "args": ["-y", "gaseo-mcp"],
      "env": {
        "GASEO_API_KEY": "ak_live_..."
      }
    }
  }
}

Restart Claude Desktop. You'll see "gaseo" in the MCP indicator bar.

Cursor

~/.cursor/mcp.json (global) or <project>/.cursor/mcp.json (per-project):

{
  "mcpServers": {
    "gaseo": {
      "command": "npx",
      "args": ["-y", "gaseo-mcp"],
      "env": { "GASEO_API_KEY": "ak_live_..." }
    }
  }
}

Cline (VS Code extension)

~/.config/cline/cline_mcp_settings.json — same JSON shape as Cursor.

Tools

ToolPurpose
list_projectsDiscovery — projects this token can reach. PAT mode returns every project you can access; project-scoped returns the single bound project.
list_sitesList sites in a project.
list_articlesPaginated article list with status filter.
get_articleFull markdown body + metadata for one article.
create_articlePublish, draft, or schedule a new article.
update_articleEdit any field on an existing article.
upload_imageUpload an image (base64 or url) → permanent CDN URL.
delete_articlePermanently delete an article and its per-article images.

Two token types

The server auto-detects which kind of token you configured at boot and adapts its tool hints accordingly.

Project-scoped — ak_live_…

Bound to one project. Set GASEO_PROJECT_ID in env and the AI can call list_sites with no arguments. The AI never has to think about which project to use. Mint these from the per-project API Keys panel.

Personal Access Token — ak_user_…

Bound to your account. Covers every project you own or are a member of. The server tells the AI to call list_projects first to discover what is reachable, then pass projectId to per-project tools. One token for your whole account — create in Account → Personal API Keys.

Optional env vars

VariableDefaultPurpose
GASEO_API_KEY—Required. Your bearer token.
GASEO_PROJECT_ID—Default project for list_sites with project-scoped tokens. Ignored for Personal Access Tokens — use list_projects instead.
GASEO_API_URLhttps://gaseo.ai/api/v1Override for staging / self-hosted setups.

Security

  • The npm package is open and contains no credentials — anyone can npm install but they can't do anything without a key from you.
  • Tokens stay in your local MCP config file. The package never phones home.
  • Revoke from the dashboard at any time → MCP calls instantly start returning 401 invalid_key.

Changelog

API and MCP changes that affect external integrations.

2026-06-10

  • New endpoint: POST /sites/{siteId}/images — upload via multipart, base64, or server-side url fetch; returns a permanent CDN URL.
  • New endpoint: DELETE /sites/{siteId}/articles/{articleId} — permanent delete + per-article image cleanup.
  • New field: inlineImages on article create/update — rehost up to 20 images and weave them into the markdown body.
  • Rate limits introduced on all v1 endpoints, with X-RateLimit-* headers and 429 + Retry-After.
  • Image fetch failures now return classified errorCodes (fetch_blocked, fetch_rate_limited, fetch_timeout, …) instead of an opaque fetch 403.
  • CDN URL passthrough: pre-uploaded image URLs are used as-is on create/update; PATCH responses now include featuredImageRehost.
  • MCP server [email protected]: new upload_image + delete_article tools, inlineImages support, and clean empty responses for prompts/list / resources/list probes.

Support

Stuck? Found a bug? Need a feature (PATCH / DELETE article, bulk import, webhooks)?

  • Email the GASEO team with the API key prefix and the request ID from your error.
  • Don't share the full token over email — only the prefix (first 12 chars: ak_live_xxxx).
  • Include the timestamp of the failing request — we keep logs to triage.