External Publications API

Token-based API for external platforms to integrate Spokegrid's powerful editor and publication system.

Secure

Two-tier token system with domain whitelisting and rate limiting.

Multi-Destination

Publish to Git, cloud storage, and webhooks simultaneously.

Fast

Event-driven caching with 1-hour TTL for optimal performance.

Base URL

https://api.spokegrid.com/api

Authentication

The API uses a two-tier authentication system for maximum security:

API Token

Server-side only
  • Long-lived (configurable expiration)
  • Scoped permissions (read, publish)
  • Rate limited per token

Session Token

Browser-safe
  • Short-lived (24 hours)
  • Single user, single article
  • Safe for URL parameters

Authentication Header

Authorization: Bearer YOUR_API_TOKEN

Never Expose API Tokens

API tokens are sensitive credentials. Always create editor sessions from your server, never expose tokens in client-side code.

Publication Management

Configure where and how your articles are published. These endpoints require CMS authentication (cookie-based).

POST/publications/configs

Create a new publication configuration

Request Body

{
  "name": "Production Blog",
  "site_id": "optional_site_id",
  "git_enabled": true,
  "git_repo_id": "repo_id",
  "git_directory": "content/articles",
  "git_format": "mdx",
  "git_use_pr": false,
  "cloud_enabled": true,
  "webhook_enabled": true,
  "webhook_url": "https://your-site.com/webhooks",
  "webhook_secret": "your_secret",
  "external_access_enabled": true,
  "allowed_domains": ["*.example.com"]
}
GET/publications/configs

List all publication configurations for the authenticated user

POST/publications/tokens

Create an API token for external access

Request Body

{
  "publication_config_id": "config_uuid",
  "name": "Production API Token",
  "scopes": ["read", "publish"],
  "allowed_domains": ["*.example.com"],
  "rate_limit_per_hour": 1000,
  "expires_at": "2026-12-31T23:59:59Z"
}

Token Shown Once

The full token is only returned once during creation. Store it securely.

External Editor Sessions

Create short-lived editor sessions for your users. These endpoints use API token authentication.

POST/external/publications/sessions

Create a short-lived editor session for an external user

Request Headers

Authorization: Bearer YOUR_API_TOKEN
Content-Type: application/json

Request Body

{
  "external_user_id": "user_123",
  "external_user_email": "editor@example.com",
  "external_user_name": "Jane Doe",
  "external_user_metadata": {
    "role": "editor",
    "department": "marketing"
  },
  "article_id": "optional_existing_article_id"
}

Response (200 OK)

{
  "id": "session_uuid",
  "session_token": "secure_random_token",
  "external_user_id": "user_123",
  "expires_at": "2025-12-19T12:00:00Z",
  "created_at": "2025-12-18T12:00:00Z"
}

Next Step

Redirect your user to: https://cms.spokegrid.com/publications/editor/{session_token}

GET/external/publications/sessions/{session_token}/verify

Validate a session token. Used internally by CMS frontend when loading the editor.

POST/external/publications/sessions/{session_token}/publish

Publish an article from an editor session. Automatically distributes to enabled destinations.

Request Body

{
  "article_id": "unique_article_identifier",
  "title": "My Amazing Article",
  "slug": "my-amazing-article",
  "content": "# Markdown content\n\nFull article...",
  "format": "mdx",
  "metadata": {
    "title": "My Amazing Article",
    "excerpt": "Brief description",
    "featured_image": "https://example.com/image.jpg",
    "tags": ["tech", "tutorial"],
    "categories": ["development"],
    "seo_title": "Custom SEO Title",
    "seo_description": "Meta description",
    "custom_fields": {}
  }
}

Response (200 OK)

{
  "id": "publication_target_uuid",
  "article_id": "unique_article_identifier",
  "article_title": "My Amazing Article",
  "article_slug": "my-amazing-article",
  "published_to_git": true,
  "git_commit_sha": "a1b2c3d4...",
  "published_to_cloud": true,
  "webhook_delivered": true,
  "published_at": "2025-12-18T14:30:00Z",
  "version": 1
}

Publishing Destinations

  • Git: Commits to repository or creates PR
  • Cloud: Stores in database for API retrieval
  • Webhook: Sends signed POST to your endpoint

Reading Articles

Retrieve published articles via API. All read endpoints support heavy caching with event-driven invalidation.

GET/external/publications/articles?limit=50&offset=0

Retrieve all published articles with pagination

Query Parameters

ParameterTypeDefaultDescription
limitinteger50Max articles (1-100)
offsetinteger0Pagination offset
GET/external/publications/articles/{article_id}

Get a specific article by its unique ID

GET/external/publications/articles/by-slug/{slug}

Get an article by its URL-friendly slug (useful for frontend routing)

Webhooks

Receive real-time notifications when articles are published. Webhooks are delivered with HMAC signatures for security.

Webhook Payload

POST https://your-webhook-url.com/endpoint

Headers:
  X-Webhook-Signature: sha256=a1b2c3d4...
  X-Webhook-Timestamp: 2025-12-18T14:30:00Z
  Content-Type: application/json

Body:
{
  "event": "publish",
  "article": {
    "id": "uuid",
    "title": "My Amazing Article",
    "slug": "my-amazing-article",
    "content": "Full content...",
    "format": "mdx",
    "metadata": {},
    "author_name": "Jane Doe",
    "author_email": "editor@example.com",
    "published_at": "2025-12-18T14:30:00Z",
    "version": 1
  },
  "publication_config_id": "config_uuid",
  "timestamp": "2025-12-18T14:30:00Z"
}

Verify Webhook Signature

Always verify the HMAC signature to ensure webhooks are from Spokegrid.

Signature Verification

const crypto = require('crypto');

function verifyWebhook(payload, signature, secret) {
  const expectedSig = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');
  
  return signature === `sha256=${expectedSig}`;
}

app.post('/webhooks/articles', (req, res) => {
  const sig = req.headers['x-webhook-signature'];
  const isValid = verifyWebhook(req.body, sig, WEBHOOK_SECRET);
  
  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }
  
  const { event, article } = req.body;
  console.log(`Received ${event}: ${article.title}`);
  
  res.status(200).send('OK');
});

Automatic Retries

Failed webhooks are automatically retried up to 3 times with exponential backoff (5min, 10min, 15min).

Reading Articles

Retrieve published articles from your publication configuration. These endpoints use API token authentication.

GET/external/publications/articles

List published articles with pagination

Query Parameters

limit: 50 (max 100, default 50)
offset: 0 (default 0)

Response (200 OK)

[
  {
    "id": "article_uuid",
    "title": "My Amazing Article",
    "slug": "my-amazing-article",
    "content": "Full markdown content...",
    "format": "mdx",
    "metadata": {
      "tags": ["tech", "tutorial"],
      "categories": ["development"]
    },
    "author_name": "Jane Doe",
    "author_email": "editor@example.com",
    "published_at": "2025-12-18T14:30:00Z",
    "version": 1
  }
]
GET/external/publications/articles/{article_id}

Get a specific published article by its ID

GET/external/publications/articles/by-slug/{slug}

Get a published article by its URL slug

GET/external/publications/analytics

Get publication analytics and statistics

Query Parameters

days: 30 (default 30, max 365)

Response (200 OK)

{
  "config_id": "config_uuid",
  "period_days": 30,
  "total_publications": 150,
  "successful_publications": 145,
  "failed_publications": 5,
  "git_publications": 140,
  "cloud_publications": 145,
  "webhook_deliveries": 142,
  "average_processing_time_ms": 1250,
  "error_rate": 0.033,
  "recent_errors": [
    {
      "timestamp": "2025-12-18T10:30:00Z",
      "error": "Git push failed: repository not found"
    }
  ],
  "daily_stats": [
    {
      "date": "2025-12-18",
      "publications": 12,
      "successful": 11,
      "failed": 1
    }
  ]
}

Security Features

Token Hashing

All tokens are SHA-256 hashed before storage. Full token only shown once.

Domain Whitelisting

Configure allowed domains per token. Supports wildcards (*.example.com).

Rate Limiting

Configurable per-token rate limits. Returns HTTP 429 when exceeded.

Audit Logging

Complete audit trail with IP, user agent, and referrer for all operations.

Rate Limits

Rate limits are configurable per API token. Default is 100 requests per hour.

Rate Limit Headers

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1703001600

Rate Limit Exceeded

When exceeded, API returns HTTP 429 with Retry-After header.

Error Codes

StatusMeaningCommon Causes
200OKRequest successful
400Bad RequestInvalid request body
401UnauthorizedInvalid or expired token
403ForbiddenDomain not whitelisted
404Not FoundResource doesn't exist
429Too Many RequestsRate limit exceeded
500Server ErrorUnexpected error

Complete Integration Example

1. Server: Create Editor Session

export async function POST(request) {
  const { userId } = await request.json();
  const user = await getUserById(userId);
  
  const response = await fetch(
    'https://api.spokegrid.com/api/external/publications/sessions',
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.SPOKEGRID_API_TOKEN}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        external_user_id: user.id,
        external_user_email: user.email,
        external_user_name: user.name
      })
    }
  );
  
  const session = await response.json();
  return Response.json({ sessionToken: session.session_token });
}

2. Frontend: Redirect to Editor

async function openEditor() {
  const res = await fetch('/api/create-editor-session', {
    method: 'POST',
    body: JSON.stringify({ userId: currentUser.id })
  });
  
  const { sessionToken } = await res.json();
  
  window.location.href = 
    `https://cms.spokegrid.com/publications/editor/${sessionToken}`;
}

3. Backend: Receive Webhook

app.post('/webhooks/spokegrid', async (req, res) => {
  const sig = req.headers['x-webhook-signature'];
  const payload = JSON.stringify(req.body);
  
  const expectedSig = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');
  
  if (sig !== `sha256=${expectedSig}`) {
    return res.status(401).send('Invalid signature');
  }
  
  const { event, article } = req.body;
  
  if (event === 'publish') {
    await Article.create({
      externalId: article.id,
      title: article.title,
      content: article.content,
      publishedAt: article.published_at
    });
  }
  
  res.status(200).send('OK');
});

Need Help?

Contact support for assistance with integration, custom rate limits, or enterprise features.