← Back to Dashboard

API Integration Guide

Complete setup guide for publishing articles to your site via AutoRanker

📖 Overview

AutoRanker uses a Push Model — when you click Generate in the dashboard, AutoRanker generates SEO articles and automatically pushes them to your site via a secure API endpoint.

🔑 How authentication works:

Your site exposes a POST /api/v1/publish endpoint protected by an AUTORANKER_API_KEY. AutoRanker sends articles to this endpoint using your key stored as PUBLISH_API_KEY in its settings.

What happens when you click Generate:

  1. AutoRanker generates SEO-optimized articles using AI
  2. AI generates a unique featured image for each article (using Nano Banana Pro)
  3. Articles include SEO metadata (title, meta description) and 2-3 internal links to your existing pages
  4. Each article + image (base64) is pushed to https://yoursite.com/api/v1/publish
  5. Your site saves the article and image to the PostgreSQL database
  6. The article becomes available at /blog/[slug] with proper <title> and <meta description> tags
  7. Image served at /api/images/[slug]
  8. Links appear in your Articles dashboard
💡 Two ways to generate articles:
  • Manual: Click Generate in the Dashboard — AutoRanker pushes articles to your site immediately.
  • Automated (Cron): Call POST /api/v1/articles/generate from a scheduled script — articles are generated and pushed automatically on a schedule. See Cron Setup →

🚀 Push Flow

Here is the complete data flow from generation to your live blog:

AutoRanker Dashboard ↓ (click Generate) AutoRanker API (generates articles + images with AI) ↓ POST /api/v1/publish ↓ Authorization: Bearer PUBLISH_API_KEY ↓ Payload: article data + imageBase64 Your Site (yoursite.com) ↓ validates AUTORANKER_API_KEY ↓ saves article + image to PostgreSQL Live at: yoursite.com/blog/[slug] Image at: yoursite.com/api/images/[slug]

Key variables:

PUBLISH_API_KEY
AutoRanker
Set in AutoRanker settings. Used to authenticate pushes to your site.
AUTORANKER_API_KEY
Your Site
Set in your site's environment variables. Must match PUBLISH_API_KEY exactly.
DATABASE_URL
Your Site
PostgreSQL connection string. If missing, falls back to local JSON (data lost on redeploy).

🔄 Automated Generation via Cron

Instead of clicking Generate manually, you can call the AutoRanker API from a scheduled script (cron job). This lets you publish new articles automatically — for example, every 3 days.

How it works:
  1. Your cron script calls POST /api/v1/articles/generate on AutoRanker with a keyword
  2. AutoRanker generates the article + image and returns it in the response
  3. Your script fetches the image and calls POST /api/v1/publish on your own site
  4. The article appears at /blog/[slug] on your site
  5. The API call is tracked in your API Usage dashboard

Generate Endpoint

POSThttps://autoranker.co/api/v1/articles/generate
// Request
POST https://autoranker.co/api/v1/articles/generate
Authorization: Bearer YOUR_AUTORANKER_API_KEY
Content-Type: application/json

{
  "keyword": "Large Language Models explained",
  "length": "medium",
  "articleType": "blog",
  "domain": "yoursite.com",
  "includeImage": true
}

// Response
{
  "id": "art_abc123",
  "status": "completed",
  "content": {
    "title": "Large Language Models Explained: A Complete Guide",
    "html": "<article>...</article>",
    "excerpt": "Learn how LLMs work...",
    "imageUrl": "data:image/png;base64,..."   // base64 data URL — extract directly
  },
  "metadata": {
    "wordCount": 1450,
    "keyword": "Large Language Models explained"
  }
}
⚠️ Image URL expires!

The imageUrl in the response is a base64 data URL (data:image/png;base64,...). Extract the base64 part directly — no HTTP fetch needed. Pass it as imageBase64 to your publish endpoint where it will be stored in PostgreSQL.

Complete Cron Script (Node.js)

Save this as scripts/generate-articles.js in your site's repo:

#!/usr/bin/env node
// Rotate through topics — one article per run
const TOPICS = [
  'Large Language Models explained',
  'Generative AI for business',
  'SaaS startups and AI trends',
  'Artificial Intelligence tools 2025',
  'Web development with AI',
]

const fs = require('fs')
const path = require('path')
const STATE_FILE = path.join(__dirname, '.generate-state.json')

function loadState() {
  try { return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8')) }
  catch { return { lastTopicIndex: -1 } }
}

async function run() {
  const AUTORANKER_KEY = process.env.AUTORANKER_API_KEY
  const SITE_URL = process.env.SITE_URL || 'https://yoursite.com'

  if (!AUTORANKER_KEY) { console.error('Missing AUTORANKER_API_KEY'); process.exit(1) }

  // Pick next topic in rotation
  const state = loadState()
  const nextIndex = (state.lastTopicIndex + 1) % TOPICS.length
  const keyword = TOPICS[nextIndex]

  console.log('Generating article for:', keyword)

  // 1. Generate article via AutoRanker API
  const genRes = await fetch('https://autoranker.co/api/v1/articles/generate', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${AUTORANKER_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ keyword, length: 'medium', articleType: 'blog', domain: new URL(SITE_URL).hostname, includeImage: true }),
  })

  if (!genRes.ok) { console.error('Generate failed:', await genRes.text()); process.exit(1) }
  const data = await genRes.json()
  if (data.status === 'failed') { console.error('Generation failed:', data.message); process.exit(1) }

  console.log('Generated:', data.content?.title, '(' + data.metadata?.wordCount + ' words)')

  // 2. Extract image base64 — imageUrl is already a data URL, no HTTP fetch needed
  let imageBase64 = null
  const rawImageUrl = data.content?.imageUrl
  if (rawImageUrl?.startsWith('data:')) {
    const match = rawImageUrl.match(/^data:[^;]+;base64,(.+)$/)
    if (match) {
      imageBase64 = match[1]
      console.log('Image extracted (' + Math.round(imageBase64.length / 1024) + 'KB)')
    }
  }

  // 3. Build slug from title
  const slug = data.content?.title?.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '')

  // 4. Publish to your site
  const pubRes = await fetch(`${SITE_URL}/api/v1/publish`, {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${AUTORANKER_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      article: {
        id: data.id, title: data.content?.title, slug,
        content: data.content?.html, metaDescription: data.content?.excerpt,
        articleType: 'blog', status: 'published',
        wordCount: data.metadata?.wordCount || 0,
        primaryKeyword: keyword, createdAt: new Date().toISOString(),
      },
      imageBase64,
    }),
  })

  if (!pubRes.ok) { console.error('Publish failed:', await pubRes.text()); process.exit(1) }
  console.log('Published to', SITE_URL + '/blog/' + slug)

  // 5. Save state for next run
  fs.writeFileSync(STATE_FILE, JSON.stringify({ lastTopicIndex: nextIndex, lastRun: new Date().toISOString() }, null, 2))
}

run().catch(err => { console.error(err); process.exit(1) })

Deploy as Railway Cron Service

Create a separate cron service in Railway that runs your script on a schedule:

  1. In Railway, open your project → + NewEmpty Service → name it e.g. article-cron
  2. Connect it to the same GitHub repo as your site
  3. In the service Settings:
    • Custom Build Command: echo "no build"
    • Custom Start Command: node scripts/generate-articles.js
    • Cron Schedule: 0 9 */3 * * (every 3 days at 9am)
  4. Add environment variables to the cron service:
    AUTORANKER_API_KEY = sk_prod_YOUR_KEY_HERE
    SITE_URL           = https://yoursite.com
    DATABASE_URL       = postgresql://...  # same as your main service
  5. Deploy — Railway will run the script on schedule. You can also trigger it immediately with Run now in the Deployments tab.
💡 Cron schedule examples:
0 9 */3 * *   — every 3 days at 9am (recommended for Starter plan)
0 9 * * *     — every day at 9am
0 9 * * 1     — every Monday at 9am

📊 API Dashboard

Every call to POST /api/v1/articles/generate is tracked in your API Usage dashboard at /dashboard/api.

What you'll see:

  • Monthly API Calls — how many articles you've generated via API this month vs. your plan limit
  • Resets on [date] — your subscription renewal date (30 days from signup, not the 1st of the month)
  • Active API Keys — keys you've created in Settings → API Keys
  • Success Rate & Avg Response Time — generation performance stats
Plan limits apply to API calls:

Each call to /api/v1/articles/generate counts as one article against your monthly limit. For the Starter plan (10 articles/month) with a cron every 3 days, you'll use ~10 articles/month — exactly at the limit.

How to create an API key:

  1. Go to Dashboard → Settings → API Keys
  2. Click Create API Key → enter a name (e.g. txt-llms.com)
  3. Copy the key (shown only once!) and save it as AUTORANKER_API_KEY in your site's environment variables

📁 Required Files

Your Next.js site needs these 8 files to receive and display articles from AutoRanker:

💡 Tip: All files below follow the same structure. Set up each one, deploy to Railway (or Vercel), and your site will be ready to receive articles from AutoRanker.

1. prisma/schema.prisma — Database Schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Article {
  id              String   @id @default(uuid())
  title           String
  slug            String   @unique
  content         String   @db.Text
  metaDescription String?  @db.Text
  featuredImage   String?
  imageData       String?  @db.Text  // base64 image stored in DB
  articleType     String   @default("blog")
  status          String   @default("published")
  wordCount       Int
  primaryKeyword  String?
  siteUrl         String?
  generatedVia    String   @default("api")
  createdAt       DateTime @default(now())
  publishedAt     DateTime?
  updatedAt       DateTime @updatedAt

  @@index([siteUrl])
  @@index([slug])
}

2. lib/storage/articles-store.ts — Storage Abstraction

Handles both PostgreSQL and JSON fallback. Key logic:

import { PrismaClient } from '@prisma/client'
import fs from 'fs/promises'
import path from 'path'

// Use DB if DATABASE_URL is set, otherwise fallback to JSON
const useDb = !!process.env.DATABASE_URL
const prisma = useDb ? new PrismaClient() : null
const STORAGE_PATH = path.join(process.cwd(), 'data', 'articles.json')

export async function saveArticle(article: StoredArticle): Promise<void> {
  if (useDb && prisma) {
    await prisma.article.upsert({
      where: { slug: article.slug },
      update: { ...article },
      create: { ...article }
    })
    return
  }
  // JSON fallback (WARNING: data lost on redeploy!)
  const articles = await getAllArticles()
  const existing = articles.findIndex(a => a.slug === article.slug)
  if (existing >= 0) articles[existing] = article
  else articles.push(article)
  await fs.writeFile(STORAGE_PATH, JSON.stringify(articles, null, 2))
}

export async function getArticleBySlug(slug: string) {
  if (useDb && prisma) {
    return await prisma.article.findUnique({ where: { slug } })
  }
  const articles = await getAllArticles()
  return articles.find(a => a.slug === slug) || null
}

3. app/api/v1/publish/route.ts — Publish Endpoint

import { NextRequest, NextResponse } from 'next/server'
import { saveArticle } from '@lib/storage/articles-store'

export async function POST(req: NextRequest) {
  const authHeader = req.headers.get('authorization')
  const serverKey = process.env.AUTORANKER_API_KEY?.trim()

  if (!authHeader || authHeader.trim() !== `Bearer ${serverKey}`) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  try {
    const data = await req.json()
    const article = data.article
    const imageBase64 = data.imageBase64

    // Store image in DB and set featuredImage to API route
    if (imageBase64) {
      article.imageData = imageBase64
      article.featuredImage = `/api/images/${article.slug}`
    }

    // Ensure wordCount
    if (!article.wordCount && article.content) {
      article.wordCount = article.content.split(/\s+/).length
    }

    await saveArticle({ ...article, status: 'published' })
    return NextResponse.json({ success: true })
  } catch (e) {
    return NextResponse.json({ error: 'Database Error' }, { status: 500 })
  }
}

3b. app/api/images/[slug]/route.ts — Image Serving

import { NextResponse } from 'next/server'
import prisma from '@lib/prisma'

export async function GET(
  _req: Request,
  { params }: { params: Promise<{ slug: string }> }
) {
  const { slug } = await params
  const article = await prisma.article.findUnique({
    where: { slug },
    select: { imageData: true }
  })

  if (!article?.imageData) {
    return new NextResponse('Not found', { status: 404 })
  }

  const buffer = Buffer.from(article.imageData, 'base64')
  return new NextResponse(new Uint8Array(buffer), {
    headers: {
      'Content-Type': 'image/png',
      'Cache-Control': 'public, max-age=31536000, immutable'
    }
  })
}

4. app/blog/page.tsx — Blog List Page

import { getArticles } from '@lib/storage/articles-store'

export default async function BlogPage() {
  const articles = await getArticles({ status: 'published' })

  return (
    <main>
      <h1>Blog</h1>
      {articles.map(article => (
        <article key={article.id}>
          <a href={`/blog/${article.slug}`}>{article.title}</a>
        </article>
      ))}
    </main>
  )
}

5. app/blog/[slug]/page.tsx — Article Page + SEO Metadata

import { notFound } from 'next/navigation'
import { Metadata } from 'next'
import { getArticleBySlug } from '@lib/storage/articles-store'

export const dynamic = 'force-dynamic'

// SEO: generates <title> and <meta description> for each article
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
  const { slug } = await params
  const article = await getArticleBySlug(slug)

  if (!article) return { title: 'Article Not Found' }

  return {
    title: article.title,
    description: article.metaDescription || undefined,
    openGraph: {
      title: article.title,
      description: article.metaDescription || undefined,
      type: 'article',
      ...(article.featuredImage ? { images: [{ url: article.featuredImage }] } : {})
    }
  }
}

// Next.js 15: params is a Promise and must be awaited!
export default async function ArticlePage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const article = await getArticleBySlug(slug)

  if (!article) return notFound()

  return (
    <article>
      {article.featuredImage && (
        <img src={article.featuredImage} alt={article.title} />
      )}
      <h1>{article.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: article.content }} />
    </article>
  )
}
🔍 SEO Metadata:

The generateMetadata function sets <title>, <meta name="description">, and OpenGraph tags for each article. Without it, search engines and social media won't display proper titles and descriptions for your pages.

⚠️ Next.js 15 Breaking Change:

In Next.js 15, params in dynamic routes is a Promise and must be awaited. Using params.slug directly will return undefined, causing 404 errors.

6. package.json — Scripts

{
  "scripts": {
    "dev": "next dev",
    "build": "prisma generate && next build",
    "start": "prisma db push --accept-data-loss && next start"
  }
}
Why prisma db push in start?

Railway runs npm start after each deploy. prisma db push creates/syncs the database tables automatically. Without this, the app starts but the Article table doesn't exist yet.

7. .env.local (local) or Railway Variables (production)

# Your site's environment variables
AUTORANKER_API_KEY=sk_prod_YOUR_KEY_HERE
DATABASE_URL=postgresql://postgres:[email protected]:5432/railway
⚠️ .env.local is NOT deployed!

The .env.local file is in .gitignore and never pushed to GitHub. You must set AUTORANKER_API_KEY and DATABASE_URL directly in your hosting provider's environment variables (Railway Variables, Vercel Environment Variables, etc.)

🔧 Environment Variables

On Your Site (txt-llms.com or your Next.js app)

AUTORANKER_API_KEY required
string
Secret key that AutoRanker uses to authenticate pushes. Must match PUBLISH_API_KEY in AutoRanker exactly (case-sensitive!).
DATABASE_URL
string
PostgreSQL connection string. If not set, articles are saved to data/articles.json (lost on redeploy). Strongly recommended for production.

In AutoRanker (this app)

PUBLISH_API_KEY required
string
The key AutoRanker sends in the Authorization: Bearer header when pushing articles. Set in .env.local of this project.
⚠️ Keys must match exactly!

If PUBLISH_API_KEY in AutoRanker is sk_prod_abc123, then AUTORANKER_API_KEY on your site must also be exactly sk_prod_abc123. Even one character difference causes 401 errors.

Where to set them:

# Option 1: .env.local (local development only, NOT deployed) AUTORANKER_API_KEY=sk_prod_YOUR_KEY_HERE DATABASE_URL=postgresql://... # Option 2: Railway Dashboard # Go to: Project → Service → Variables → + New Variable # Option 3: Vercel Dashboard # Go to: Project → Settings → Environment Variables # Option 4: VPS / Docker # Add to your docker-compose.yml or systemd service file

🗄️ Database Setup

PostgreSQL is required for production. Articles persist across deploys.

Option A: Railway PostgreSQL (easiest)

  1. In Railway, open your project
  2. Click + NewDatabasePostgreSQL
  3. Railway automatically adds DATABASE_URL to your service variables
  4. The internal URL format: postgresql://postgres:[email protected]:5432/railway
  5. Redeploy your service — prisma db push will create the tables

Option B: Supabase (free tier)

  1. Create a project at supabase.com
  2. Go to Settings → Database → Connection String
  3. Copy the URI and set it as DATABASE_URL
  4. Run npx prisma db push locally (with the public URL)

Option C: Any PostgreSQL

# Set DATABASE_URL in your environment
DATABASE_URL=postgresql://USER:PASSWORD@HOST:5432/DATABASE

# Then run (once) to create tables:
npx prisma db push

# Or use migrations:
npx prisma migrate deploy
⚠️ JSON fallback is for local development only!

When DATABASE_URL is not set, articles are saved to data/articles.json. This file is wiped on every redeploy on Railway, Vercel, or any container-based hosting. All articles will be lost.

🚂 Deploy to Railway

Full step-by-step guide to deploy your Next.js site on Railway with PostgreSQL.

Step 1: Push your code to GitHub

cd your-nextjs-site
git init
git add -A
git commit -m "initial commit"
git remote add origin [email protected]:YOUR_USERNAME/YOUR_REPO.git
git push -u origin main

Step 2: Create Railway project

  1. Go to railway.comNew Project
  2. Select Deploy from GitHub repo
  3. Choose your repository
  4. Railway auto-detects Next.js and sets up the build

Step 3: Add PostgreSQL

  1. In your project, click + NewDatabaseAdd PostgreSQL
  2. Railway automatically links it and adds DATABASE_URL to your service

Step 4: Set environment variables

Go to your service → Variables → add:

AUTORANKER_API_KEY = sk_prod_YOUR_KEY_HERE
# DATABASE_URL is already added automatically by Railway

Step 5: Verify package.json start script

"start": "prisma db push --accept-data-loss && next start"

This ensures database tables are created on every deploy.

Step 6: Deploy

git push origin main  # Railway auto-deploys on push

Step 7: Generate articles

  1. Open AutoRanker Dashboard
  2. Set your domain to yoursite.railway.app (or custom domain)
  3. Click Generate
  4. Articles are pushed to your site and appear at /blog/[slug]
  5. View published links in Dashboard → Articles
💡 Custom domain:

In Railway → your service → SettingsDomains → add your custom domain. Update your domain's DNS to point to Railway.

▲ Deploy to Vercel / VPS

Vercel

  1. Push your code to GitHub
  2. Go to vercel.comNew Project → import your repo
  3. In Environment Variables, add:
    AUTORANKER_API_KEY = sk_prod_YOUR_KEY_HERE
    DATABASE_URL = postgresql://...
  4. Deploy
  5. Run migrations once: npx prisma db push (with your production DATABASE_URL)
⚠️ Vercel note:

Vercel's filesystem is read-only. JSON fallback will NOT work on Vercel. You must use PostgreSQL (Supabase, Neon, or any external PostgreSQL).

VPS (Ubuntu/Debian)

# Clone your repo
git clone [email protected]:YOUR_USERNAME/YOUR_REPO.git
cd YOUR_REPO

# Set environment variables
echo "AUTORANKER_API_KEY=sk_prod_YOUR_KEY_HERE" >> .env
echo "DATABASE_URL=postgresql://..." >> .env

# Install and build
npm install
npx prisma db push
npm run build

# Start with PM2
npm install -g pm2
pm2 start "npm start" --name "my-blog"
pm2 save

📦 Setup Without GitHub

If you don't want to use GitHub, you can deploy directly via Railway CLI or upload files manually.

Option A: Railway CLI (no GitHub needed)

# Install Railway CLI
npm install -g @railway/cli

# Login
railway login

# Create new project
railway init

# Link to existing project
railway link

# Set environment variables
railway variables set AUTORANKER_API_KEY=sk_prod_YOUR_KEY_HERE
railway variables set DATABASE_URL=postgresql://...

# Deploy directly from local folder
railway up

Option B: Manual upload to VPS

# Build locally
npm run build

# Upload to server via SCP
scp -r .next package.json public user@your-server:/var/www/myblog/

# On the server
cd /var/www/myblog
npm install --production
AUTORANKER_API_KEY=sk_prod_... DATABASE_URL=postgresql://... npm start

Option C: Docker

# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install && npm run build
EXPOSE 3000
CMD ["npm", "start"]

# docker-compose.yml
version: '3'
services:
  web:
    build: .
    environment:
      - AUTORANKER_API_KEY=sk_prod_YOUR_KEY_HERE
      - DATABASE_URL=postgresql://postgres:password@db:5432/blog
    ports:
      - "3000:3000"
  db:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: password
      POSTGRES_DB: blog

📡 POST /api/v1/publish

AutoRanker calls this endpoint on your site to publish each generated article.

POSThttps://yoursite.com/api/v1/publish

Request Headers

Authorization required
string
Bearer YOUR_AUTORANKER_API_KEY
Content-Type required
string
application/json

Request Body

{
  "article": {
    "id": "uuid",
    "title": "Best AI Tools in 2025",
    "slug": "best-ai-tools-2025",
    "content": "<article>...</article>",
    "metaDescription": "Discover the best AI tools...",
    "featuredImage": "/api/images/best-ai-tools-2025",
    "articleType": "blog",
    "wordCount": 1450,
    "primaryKeyword": "ai tools",
    "siteUrl": "yoursite.com",
    "generatedVia": "api"
  },
  "imageBase64": "iVBORw0KGgo..."  // optional, base64-encoded PNG
}
🖼️ Image handling:

When imageBase64 is provided, the image is stored in PostgreSQL and served via /api/images/[slug]. Images persist across deploys — no external storage needed.

Response

// Success
{ "success": true }

// Error - wrong API key
{ "error": "Unauthorized" }  // 401

// Error - database issue
{ "error": "Database Error" }  // 500

Example curl test

curl -X POST https://yoursite.com/api/v1/publish \
  -H "Authorization: Bearer sk_prod_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "article": {
      "id": "test-123",
      "title": "Test Article",
      "slug": "test-article",
      "content": "<p>Hello world</p>",
      "articleType": "blog",
      "wordCount": 2,
      "siteUrl": "yoursite.com",
      "generatedVia": "api"
    }
  }'

# Expected: {"success":true}
# Then visit: https://yoursite.com/blog/test-article

📡 POST /api/v1/articles/generate

Generates a single SEO article with an optional featured image.

POSThttps://autoranker.co/api/v1/articles/generate

Request Headers

Authorization required
string
Bearer YOUR_AUTORANKER_API_KEY
Content-Type required
string
application/json

Request Body

keyword required
string
The primary keyword/topic for the article. E.g. "Large Language Models explained"
length
string
"short" | "medium" | "long". Default: "medium" (~1200–1600 words)
articleType
string
"blog" | "seo". Default: "blog"
domain
string
Your site's domain (without https://). Used for internal links in the article. E.g. "yoursite.com"
includeImage
boolean
Whether to generate a featured image. Default: true. Adds ~10s to generation time.

Response

{
  "id": "art_abc123",
  "status": "completed",        // "completed" | "failed"
  "content": {
    "title": "Large Language Models Explained",
    "html": "<article><h2>...</h2>...</article>",
    "excerpt": "Learn how LLMs work and why they matter...",
    "imageUrl": "data:image/png;base64,..."   // base64 data URL
  },
  "metadata": {
    "wordCount": 1450,
    "keyword": "Large Language Models explained",
    "generatedAt": "2026-03-26T09:00:00.000Z"
  },
  "message": "Article generated successfully"
}

// On failure:
{ "status": "failed", "message": "Rate limit exceeded" }
⏱️ Response time:

Article generation takes approximately 30–60 seconds. Make sure your HTTP client timeout is set to at least 90 seconds. The Railway cron service handles this correctly by default.

⚠️ Error Codes

401
Unauthorized

API keys don't match. Check that PUBLISH_API_KEY in AutoRanker equals AUTORANKER_API_KEY on your site (case-sensitive).

404
Article not found

Article was pushed but not saved to DB. Usually means DATABASE_URL is missing and JSON fallback was used — data was lost on redeploy. Add PostgreSQL.

500
Database Error

Database connection failed. Check DATABASE_URL is correct and prisma db push has been run to create tables.

Application Error on /blog/[slug]

The blog page is calling prisma.article.findUnique directly instead of getArticleBySlug(). Replace with the storage abstraction function.