Blog API

The Inkie Blog API has two complementary surfaces. Pick the one that matches what you're trying to do — they share the same auth (API key) but have different shapes optimised for different jobs.

Create & manage

You want to programmatically create blogs in Inkie — push complete drafts in, or have Inkie's AI generate them from a topic.

→ /api/blogs/* endpoints

Read & render

You want to pull your Inkie blogs into your own site, RSS reader, or headless CMS — list, paginate, fetch with auto-generated SEO schema.

→ /api/client-blogs/* endpoints

Base URL: https://app.inkie.ink

Create & manage

This section is for integrators driving Inkie programmatically — generating content, scheduling, polling progress.

POST/api/blogs/create

Create a blog post from complete data. Title and body are required. Returns immediately with SCHEDULED status.

Request body
{
  "platforms": ["wix"],          // Required. Blog platforms to publish to.
  "scheduledDate": "2025-10-20T10:00:00",  // Required. ISO datetime, local time (no tz suffix).
  "title": "5 tips for better content",    // Required.
  "body": "<p>Your full HTML body...</p>", // Required.
  "metaDescription": "Optional SEO description",
  "clientId": 123,               // Optional. Defaults to your account's active client.
  "contentPlanId": 456           // Optional. Associate with a content plan.
}
Response 200
{
  "status": "SCHEDULED",
  "blogId": 789,
  "planId": 456
}
POST/api/blogs/generate

Generate a blog post with AI. Provide a topic — Inkie writes the full article including title, body, and meta description. Returns 202; poll statusUrl for progress.

Request body
{
  "platforms": ["wix"],
  "scheduledDate": "2025-10-21T10:00:00",
  "topic": "How to repurpose video content for social media",
  "clientId": 123,
  "contentPlanId": 456
}
Response 202
{
  "status": "PROCESSING",
  "blogId": 790,
  "planId": 456,
  "statusUrl": "/api/blogs/790/status"
}
GET/api/blogs/{id}/status

Poll generation status for a blog created with generate. Poll until state is SCHEDULED.

Path param: id — the blogId returned from create or generate.

Response 200
{
  "state": "PROCESSING",  // PROCESSING | SCHEDULED | FAILED
  "progress": 60,
  "steps": [
    { "name": "writing", "state": "complete" },
    { "name": "scheduling", "state": "in_progress" }
  ]
}
GET/api/blogs/{id}

Get full blog details — title, body, meta description, scheduled date in your client timezone.

Path param: id — the blogId.

Response 200
{
  "id": 790,
  "title": "How to repurpose video content",
  "body": "<p>Full HTML blog body...</p>",
  "metaDescription": "Learn how to repurpose...",
  "scheduledDate": "2025-10-21T10:00:00",
  "status": "SCHEDULED",
  "platforms": ["wix"]
}

Hand this to your AI builder

Wiring up a workflow that pushes content INTO Inkie — from your CMS, a Notion sync, a podcast-summariser cron, a Slack /post command? Paste the prompt below into Cursor, Bolt, Lovable, or whichever AI builder you're using.

Replace YOUR_API_KEY with your actual key from Settings → API keys.

Inkie Blog Creation Promptpaste me
I want to push content into my Inkie blog programmatically. My system has the content (titles, body, scheduled dates); Inkie owns the publishing pipeline. Here's everything you need:

## API

Base URL: https://app.inkie.ink

Auth: Bearer token in the Authorization header.
  Authorization: Bearer YOUR_API_KEY

### Option 1 — Create a blog from complete content I already have
POST /api/blogs/create
  Body: {
    platforms: ["wix"],                       // required, see /api/platforms for the list of yours
    scheduledDate: "2025-10-20T10:00:00",     // required, ISO datetime, LOCAL time (no tz suffix)
    title: "...",                             // required
    body: "<p>Full HTML body...</p>",         // required
    metaDescription: "Optional SEO description",
    clientId: 123,                            // optional, defaults to my account's active client
    contentPlanId: 456                        // optional, associate with a content plan
  }
  Response 200: { status: "SCHEDULED", blogId, planId }

### Option 2 — Have Inkie's AI generate the blog from a topic (async)
POST /api/blogs/generate
  Body: {
    platforms: ["wix"],
    scheduledDate: "2025-10-21T10:00:00",
    topic: "...",                             // required, the brief
    clientId: 123,
    contentPlanId: 456
  }
  Response 202: { status: "PROCESSING", blogId, planId, statusUrl: "/api/blogs/{blogId}/status" }

### Poll generation status (only needed after generate)
GET /api/blogs/{blogId}/status
  Response 200: {
    state: "PROCESSING" | "SCHEDULED" | "FAILED",
    progress: 0–100,
    steps: [{ name, state }, ...]
  }
  Poll every 3–5 seconds until state === "SCHEDULED".

### Retrieve a blog after creation
GET /api/blogs/{blogId}
  Response 200: { id, title, body, metaDescription, scheduledDate, status, platforms }

### Error shape
{ error: "..." }   // any non-2xx

## What I want you to build

1. A small helper module `inkie-blog-client.ts` (or equivalent for my language) exposing two functions:
   - `createBlog(input)` — wraps POST /api/blogs/create
   - `generateBlog(input)` — wraps POST /api/blogs/generate AND polls /status until completion (or surfaces FAILED), then fetches the final blog via GET /api/blogs/{id}

2. Both functions should:
   - Throw / return Result with the API's error message on non-2xx
   - Be small, dependency-free (use native fetch)
   - Read API key + base URL from environment variables (INKIE_API_KEY, INKIE_BASE_URL — default https://app.inkie.ink)

3. A worked example showing how to call `createBlog` from my existing content pipeline. Ask me where my content currently lives (CMS / Notion / Markdown files / etc.) so the example slots into MY shape, not a generic one.

## Critical details (Inkie-specific gotchas)

- `scheduledDate` is local time, NOT UTC. Pass "2025-10-20T10:00:00" — no `Z`, no tz offset. Inkie converts to UTC internally using my client's timezone setting.
- `platforms` accepts only platforms I've connected. Call GET /api/platforms first if unsure which strings are valid for my account.
- `generate` is async: response is 202 not 200, and the blog won't exist at /api/blogs/{id} until polling shows SCHEDULED.
- `body` for /create must be valid HTML. Markdown will publish as escaped HTML — convert first if your source is Markdown.

## Things to NOT do

- Don't poll /status faster than every 3 seconds — be a good citizen.
- Don't retry on 4xx errors — they're configuration mistakes, not transient.
- Don't hardcode the API key in source. Use env vars and document them in the README.

## Reference

Full docs: https://app.inkie.ink/docs/blog-api
OpenAPI spec: https://app.inkie.ink/api/openapi.json

Now ask me what language I'm working in (TypeScript / Python / Go / etc.) and where my content currently lives, then scaffold the helper module and worked example.

Read & render

This section is for integrators rendering Inkie blogs on their own surface — headless CMS, static-site builds, RSS feeds, mobile app screens. By default only published blogs are returned, but you can opt-in to unpublished/scheduled-future posts via ?published=false — useful for staging previews where you want to render upcoming content before it goes live.

Returns include schema.org/BlogPosting JSON-LD per blog (auto-generated from your client's profile) so you can drop it straight into an article page's <head> for SEO.

GET/api/client-blogs

List blogs for your account, newest first. Cached publicly for 5 minutes.

Query params (all optional):

  • page — 1-indexed page number. Default 1.
  • perPage — page size, 1–100. Default 100.
  • publishedtrue (default) returns published blogs only; false returns unpublished / scheduled-future drafts. Pass ?published=false for preview / staging surfaces.
  • search — case-insensitive substring match on title.
  • scheduledFrom — ISO date. Only blogs scheduled on or after this.
  • scheduledTo — ISO date. Only blogs scheduled on or before this.
Response 200
{
  "success": true,
  "data": [
    {
      "id": 790,
      "title": "How to repurpose video content",
      "slug": "how-to-repurpose-video-content",
      "metaDescription": "Learn how to repurpose...",
      "publishedAt": "2025-10-21T10:00:00.000Z",
      "scheduledDate": "2025-10-21T10:00:00.000Z",
      "status": "PUBLISHED",
      "excerpt": "Three ways to turn one recording into a week of content...",
      "mainImage": {
        "id": 42,
        "url": "https://...",
        "alt": "Recording setup",
        "title": null
      },
      "readingTime": 4
    }
  ],
  "count": 1,
  "page": 1,
  "perPage": 100
}
GET/api/client-blogs/{blogId}

Get a single published blog with full body, all images, and pre-built JSON-LD. Cached publicly for 10 minutes.

Path param: blogId — numeric blog id from the list endpoint.

Response 200
{
  "success": true,
  "data": {
    "id": 790,
    "title": "How to repurpose video content",
    "slug": "how-to-repurpose-video-content",
    "metaDescription": "Learn how to repurpose...",
    "pageTitle": "How to repurpose video content | Inkie",
    "body": "<p>Full HTML blog body...</p>",
    "publishedAt": "2025-10-21T10:00:00.000Z",
    "scheduledDate": "2025-10-21T10:00:00.000Z",
    "status": "PUBLISHED",
    "readingTime": 4,
    "wordCount": 850,
    "additionalImages": [ ... ],
    "structuredData": {
      "@context": "https://schema.org",
      "@type": "BlogPosting",
      "headline": "How to repurpose video content",
      "description": "Learn how to repurpose...",
      "image": ["https://..."],
      "datePublished": "2025-10-21T10:00:00.000Z",
      "dateModified": "2025-10-21T10:00:00.000Z",
      "author": { "@type": "Organization", "name": "Your client name" },
      "publisher": { "@type": "Organization", "name": "Your client name" },
      "mainEntityOfPage": { "@type": "WebPage", "@id": "https://..." },
      "wordCount": 850
    }
  }
}

Use cases

  • Headless CMS: fetch list at build time, render each post on your site's templates. Cache headers cooperate with Next.js / Astro ISR.
  • RSS / Atom feed: map the list response straight to feed items.
  • Article SEO: drop structuredData into <script type="application/ld+json"> in your article <head>.
  • Mobile app: paginate via page/perPage; cache the list locally between visits.

Hand this to your AI site-builder

Using Cursor, v0, Bolt, Lovable, or another AI builder to scaffold your site? Copy the prompt below into the chat — it gives the AI everything it needs to wire up your Inkie blog as a headless CMS feed: list page, article page, JSON-LD SEO, the lot.

Replace YOUR_API_KEY with your actual key from Settings → API keys. The prompt assumes a modern React/Remix/Next/Astro frontend; tweak the framework section if you're using something else.

Inkie Blog Integration Promptpaste me
I want to integrate my Inkie blog into this site as a headless CMS feed. Inkie publishes my blog posts; my site fetches and renders them. Here's everything you need:

## API

Base URL: https://app.inkie.ink

Auth: Bearer token in the Authorization header.
  Authorization: Bearer YOUR_API_KEY

### List blogs
GET /api/client-blogs?page=1&perPage=20
  Optional query params:
    published (default true; pass false for unpublished/scheduled drafts — preview/staging only)
    search (substring match on title)
    scheduledFrom, scheduledTo (ISO dates)
  Response: { success: true, data: BlogSummary[], count, page, perPage }
  BlogSummary: { id, title, slug, metaDescription, publishedAt, scheduledDate,
                 status, excerpt, mainImage?: { id, url, alt, title },
                 readingTime }
  Cache: public, max-age=300 (5 min). Sort: newest first.

### Get a single published blog
GET /api/client-blogs/{blogId}
  Response: { success: true, data: BlogDetail }
  BlogDetail includes: title, slug, body (HTML string), pageTitle,
    metaDescription, publishedAt, scheduledDate, additionalImages[],
    structuredData (a ready-made schema.org/BlogPosting JSON-LD object),
    readingTime, wordCount.
  Cache: public, max-age=600 (10 min).

### Error shape
{ success: false, error: "..." }   // any non-2xx

## What I want you to build

1. A blog index page at /blog that:
   - Fetches GET /api/client-blogs?perPage=20 (server-side / at build time)
   - Renders a card grid with title, excerpt, mainImage.url, readingTime, publishedAt
   - Links each card to /blog/{slug}

2. A blog detail page at /blog/{slug} that:
   - Fetches GET /api/client-blogs/{id} (resolve slug → id via the list, or pass id in URL — whichever your router prefers)
   - Renders the body HTML directly (it's safe — sanitised on Inkie's side)
   - Adds the entire structuredData object into the page's <head> as:
       <script type="application/ld+json">{JSON.stringify(structuredData)}</script>
   - Sets pageTitle as the <title>, metaDescription as <meta name="description">
   - Optionally renders additionalImages somewhere in the layout

3. Style it to match the rest of the site. Use the site's existing typography + card components.

4. Where caching matters (Next.js ISR, Astro static fetch, Remix loader cache), respect Inkie's Cache-Control headers — don't fetch on every request.

## Things to NOT do

- Don't fetch on every page load — use ISR / static / loader-cache with a 5-10 min TTL
- Don't try to write through this API — it's read-only. Inkie creates blogs; you render them.
- Don't strip the structuredData — it's the SEO win, drop it into <head> exactly as returned.

## Reference

Full docs: https://app.inkie.ink/docs/blog-api
OpenAPI spec: https://app.inkie.ink/api/client-blogs/openapi.json

Now ask me which framework I'm using (Next.js, Astro, Remix, Nuxt, SvelteKit, plain HTML, etc.) and what URL my site lives on, then scaffold the two pages above.

Platforms

The platforms field accepts any combination of your connected blog platforms. To see which platforms are active on your account:

GET /api/platforms
Authorization: Bearer YOUR_API_KEY

Returns a list of enabled blog and social platforms.

Scheduled dates

Pass scheduledDate as an ISO datetime string without a timezone suffix — e.g. 2025-10-20T10:00:00. Inkie interprets it as your client's local time and converts to UTC internally.

Polling pattern

Endpoints that generate AI content return 202 with a statusUrl. Poll that URL every 3–5 seconds until state === "SCHEDULED".

// Pseudocode
const { blogId, statusUrl } = await fetch('/api/blogs/generate', { ... }).json();

let status;
do {
  await sleep(3000);
  status = await fetch(statusUrl).json();
} while (status.state === 'PROCESSING');

const blog = await fetch(`/api/blogs/${blogId}`).json();

Error codes

StatusMeaning
400Validation error — check request body.
401Missing or invalid API key. See Authentication.
403Access denied — resource doesn't belong to your account.
404Blog not found.

See also