> ## Documentation Index
> Fetch the complete documentation index at: https://docs.neuraldraft.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Create a blog post

> Two creation modes:

1. **Manual** — provide `title`, `content` (HTML), `excerpt`, etc. The
   post is saved immediately as a draft. Costs 0 credits.
2. **AI generation** — provide `ai: { topic, style?, word_count?,
   translate_to_all?, primary_keyword?, ... }`. Returns a
   [`Job`](#tag/Jobs) with `type: blog_post.generate`. The post
   appears in the database when the job completes. Costs **60 credits**
   (`blog_post` — covers research, draft, and featured image; per-language
   translations are billed separately at **7 credits per language**).




## OpenAPI

````yaml /openapi.yaml post /blog-posts
openapi: 3.1.0
info:
  title: Neural Draft Project API
  version: 1.0.1
  summary: The AI-native backend platform for Lovable / Claude / v0 / Bolt-built sites.
  description: |
    The **Neural Draft Project API** is the single REST contract every AI-built
    site, MCP server tool call, and SDK in our ecosystem speaks against. One
    project key (`ndsk_live_...`) gets you CMS, blog, social, booking and
    commerce primitives behind a stable v1 surface.

    ## Quick start

    ```bash
    curl https://api.neuraldraft.io/v1/projects/me \
      -H "Authorization: Bearer ndsk_live_yourkey"
    ```

    ## Authentication

    Every endpoint except `GET /health` requires a project API key passed as
    `Authorization: Bearer ndsk_live_...`. Keys are scoped to a single project
    (formerly "tenant"). Resolve the project context by calling
    `GET /projects/me`.

    Keys carry one or more **scopes** (e.g. `content:write`, `commerce:admin`).
    A `403 Forbidden` is returned if the key lacks the scope required by the
    endpoint.

    ## Credits

    Most operations cost credits. Costs are documented per endpoint in this
    spec and on the [pricing page](https://neuraldraft.io/pricing). When a
    project has no credits left, every credit-consuming operation returns
    `402 Payment Required` with `code: insufficient_credits`. The response
    includes `cost` (credits the operation needed) and `balance` (credits the
    project had on hand) so clients can render an exact top-up prompt.

    Read-only endpoints (`GET`) do not consume credits. Mutating writes —
    upserting a content key, creating or updating a page, registering an
    image (URL swap or multipart upload) — cost **1 credit** each on top of
    the AI-generation costs documented per endpoint.

    ## Rate limits

    Default project budget is **60 requests per minute**, burst **120**. Every
    response includes:

    | Header | Meaning |
    |---|---|
    | `X-RateLimit-Limit` | Window ceiling |
    | `X-RateLimit-Remaining` | Requests left in window |
    | `X-RateLimit-Reset` | Unix seconds when bucket refills |
    | `Retry-After` | Seconds to wait, only on `429` |

    Enterprise plans get higher budgets — contact sales.

    ## Async work

    Long-running operations (blog generation, image generation, batch
    translation, content plans, full website generation) return a
    [`Job`](#tag/Jobs) resource immediately with `202 Accepted` and
    `status: pending`. Two ways to track progress:

    1. **Poll** `GET /jobs/{id}` until `status` is one of `completed`,
       `failed` or `cancelled`.
    2. **Stream** `GET /jobs/{id}/stream` for a Server-Sent Events feed of
       `progress`, `step`, `done`, `error` events.

    ## Webhooks

    Outgoing webhooks let your project URL receive events (e.g.
    `order.paid`, `blog_post.published`). Manage delivery URLs at
    [`/webhook-endpoints`](#tag/Webhooks). Every delivery is signed with
    HMAC-SHA256:

    ```
    X-Neural-Draft-Signature: t=1745000000,v1=<hex_hmac_sha256(body, secret)>
    X-Neural-Draft-Event: order.paid
    X-Neural-Draft-Delivery: <uuid>
    ```

    Verify the timestamp is within 5 minutes and the HMAC matches before
    trusting the payload.

    ## Idempotency

    Mutating endpoints accept an `Idempotency-Key` header (any unique string
    up to 255 chars). Retries with the same key within 24 hours return the
    original response without re-executing the side effect.

    ## Errors

    All errors follow [RFC 7807](https://datatracker.ietf.org/doc/html/rfc7807)
    `application/problem+json` shape. The `code` field is a stable machine
    identifier; the `detail` is a human-readable explanation.
  termsOfService: https://neuraldraft.io/legal/terms
  contact:
    name: Neural Draft Support
    email: info@neuraldraft.io
    url: https://neuraldraft.io/support
  license:
    name: Proprietary
    url: https://neuraldraft.io/legal/api-license
  x-logo:
    url: https://neuraldraft.io/assets/logo-dark.svg
    altText: Neural Draft
servers:
  - url: https://api.neuraldraft.io/v1
    description: Production
  - url: https://api.staging.neuraldraft.io/v1
    description: Staging
security:
  - apiKeyAuth: []
tags:
  - name: Health
    description: Liveness, version, and the runtime OpenAPI document.
  - name: Projects
    description: >
      Project (formerly "tenant") metadata, API key management, and credit
      usage.

      Every API key resolves to exactly one project.
  - name: Brand
    description: |
      Canonical brand context — voice, colors, fonts, audience, content tone.
      This is the single source of truth that the MCP server's `brand://current`
      resource exposes to AI codegen tools.
  - name: Content
    description: |
      The CMS pillar. Translation keys map `dot.notation` keys to per-language
      values. Use `bulk` for build-time fetches of many keys at once.
  - name: Components
    description: >
      Editable HTML chunks registered by AI codegen tools during a build
      session.

      A registered component becomes editable in the project's admin UI.
  - name: Pages
    description: |
      Multi-page authoring with per-page SEO meta (meta_title, meta_description,
      og_*, canonical_url). Pages live alongside the components that render
      them.
  - name: Galleries
    description: |
      Named, ordered image collections. One `slug` per gallery, an `items`
      array of `{url, alt}` (max 200). Updates are full-replace — fetch,
      mutate, send the complete new list back. Use for carousels, lookbooks,
      product galleries, and any other variable-length image set.
  - name: Images
    description: Brand-consistent image generation, replacement, and resolution by key.
  - name: Blog
    description: |
      Full blog engine — posts, categories, tags, scheduling, multi-language
      translation, AI authoring pipeline.
  - name: Social
    description: |
      Social pillar — multi-platform post generation, scheduling, publishing,
      OAuth account connections.
  - name: Booking
    description: |
      Booking pillar — bookable services, weekly availability, slot lookup,
      bookings (admin + public), embeddable widget.
  - name: Commerce
    description: |
      Commerce pillar — products, variants, categories, orders, Stripe
      Checkout, Stripe Connect onboarding, embeddable widgets.
  - name: Jobs
    description: |
      Async work tracking. Every long-running AI op (blog gen, image gen,
      translation, content plan, website gen) returns a Job. Poll or stream.
  - name: Webhooks
    description: |
      Outgoing webhooks. Subscribe a URL to events your project cares about.
      All deliveries signed with HMAC-SHA256.
  - name: Forms
    description: |
      Lead-capture surfaces — newsletter subscribe and contact-form submit.
      Public submit endpoints (no auth, project resolved via
      `X-NeuralDraft-Project-Key` or `?project_id=`); admin list/delete
      endpoints require an API key with `forms:read` / `forms:write` scope.
      No credits charged — this is storage + delivery, not AI.
paths:
  /blog-posts:
    post:
      tags:
        - Blog
      summary: Create a blog post
      description: |
        Two creation modes:

        1. **Manual** — provide `title`, `content` (HTML), `excerpt`, etc. The
           post is saved immediately as a draft. Costs 0 credits.
        2. **AI generation** — provide `ai: { topic, style?, word_count?,
           translate_to_all?, primary_keyword?, ... }`. Returns a
           [`Job`](#tag/Jobs) with `type: blog_post.generate`. The post
           appears in the database when the job completes. Costs **60 credits**
           (`blog_post` — covers research, draft, and featured image; per-language
           translations are billed separately at **7 credits per language**).
      operationId: createBlogPost
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/BlogPostManualInput'
                - $ref: '#/components/schemas/BlogPostAIInput'
            examples:
              manual:
                summary: Manual draft
                value:
                  title: Why Mondays don't have to suck
                  slug: why-mondays-dont-have-to-suck
                  content: <p>Here's a simple framework…</p>
                  excerpt: A simple framework for resetting the week.
                  language_code: en
                  category_id: 7
                  status: draft
              ai:
                summary: AI-generated post
                value:
                  ai:
                    topic: 5-minute breathwork for anxious mornings
                    style: friendly_professional
                    word_count: 1200
                    primary_keyword: morning breathwork
                    secondary_keywords:
                      - box breathing
                      - nervous system reset
                    translate_to_all: true
                    enable_research: true
                    image_style: warm_lifestyle_photo
      responses:
        '201':
          description: Manual post created (synchronous).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BlogPost'
        '202':
          description: AI generation job queued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Job'
              examples:
                queued:
                  value:
                    id: job_2Ngd9KqLmRpW
                    type: blog_post.generate
                    status: pending
                    progress: 0
                    created_at: '2026-04-19T10:14:02Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
      x-codeSamples:
        - lang: curl
          label: cURL (AI)
          source: |
            curl -X POST https://api.neuraldraft.io/v1/blog-posts \
              -H "Authorization: Bearer ndsk_live_yourkey" \
              -H "Content-Type: application/json" \
              -d '{"ai":{"topic":"5-minute breathwork","word_count":1200}}'
        - lang: node
          label: Node (SDK)
          source: >
            const job = await nd.blogPosts.generate({ topic: "5-minute
            breathwork", wordCount: 1200 });

            const result = await nd.jobs.wait(job.id);
        - lang: python
          label: Python
          source: |
            job = requests.post(
                "https://api.neuraldraft.io/v1/blog-posts",
                headers={"Authorization": f"Bearer {key}"},
                json={"ai": {"topic": "5-minute breathwork", "word_count": 1200}},
            ).json()
        - lang: php
          label: PHP
          source: |
            <?php
            $job = json_decode((string) $client->post('blog-posts', [
              'headers' => ['Authorization' => 'Bearer '.$key],
              'json' => ['ai' => ['topic' => '5-minute breathwork', 'word_count' => 1200]],
            ])->getBody(), true);
components:
  schemas:
    BlogPostManualInput:
      type: object
      required:
        - title
        - content
        - language_code
      properties:
        title:
          type: string
        slug:
          type: string
          description: Optional. Auto-generated if omitted.
        content:
          type: string
          description: HTML body.
        excerpt:
          type: string
        meta_title:
          type: string
        meta_description:
          type: string
        language_code:
          type: string
          example: en
        category_id:
          type: integer
        tag_ids:
          type: array
          items:
            type: integer
        featured_image:
          type: string
          format: uri
        status:
          type: string
          enum:
            - draft
            - published
            - scheduled
          default: draft
    BlogPostAIInput:
      type: object
      required:
        - ai
      properties:
        ai:
          type: object
          required:
            - topic
          properties:
            topic:
              type: string
              example: 5-minute breathwork for anxious mornings
            style:
              type: string
              example: friendly_professional
            word_count:
              type: integer
              default: 1200
              minimum: 200
              maximum: 5000
            translate_to_all:
              type: boolean
              default: false
            additional_instructions:
              type: string
            target_audience:
              type: string
            primary_keyword:
              type: string
            secondary_keywords:
              type: array
              items:
                type: string
            image_style:
              type: string
              example: warm_lifestyle_photo
            enable_research:
              type: boolean
              default: true
            research_depth:
              type: string
              enum:
                - shallow
                - normal
                - deep
              default: normal
            reference_urls:
              type: array
              items:
                type: string
                format: uri
            category_id:
              type: integer
            tag_ids:
              type: array
              items:
                type: integer
    BlogPost:
      type: object
      required:
        - id
        - slug
        - status
        - title
      properties:
        id:
          type: integer
          example: 142
        slug:
          type: string
          example: 5-minute-breathwork-for-anxious-mornings
        status:
          type: string
          enum:
            - draft
            - published
            - scheduled
            - archived
          example: published
        featured_image:
          type:
            - string
            - 'null'
          format: uri
        published_at:
          type:
            - string
            - 'null'
          format: date-time
        scheduled_at:
          type:
            - string
            - 'null'
          format: date-time
        title:
          type: string
        excerpt:
          type:
            - string
            - 'null'
        content:
          type:
            - string
            - 'null'
          description: >-
            HTML for the requested language. Returned by `GET
            /blog-posts/{id_or_slug}` only.
        meta_title:
          type:
            - string
            - 'null'
        meta_description:
          type:
            - string
            - 'null'
        category:
          oneOf:
            - $ref: '#/components/schemas/Category'
            - type: 'null'
        tags:
          type: array
          items:
            $ref: '#/components/schemas/Tag'
        translations:
          type: array
          items:
            $ref: '#/components/schemas/BlogPostTranslation'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    Job:
      type: object
      required:
        - id
        - type
        - status
        - created_at
      properties:
        id:
          type: string
          example: job_2Ngd9KqLmRpW
        type:
          $ref: '#/components/schemas/JobType'
        status:
          $ref: '#/components/schemas/JobStatus'
        progress:
          type: integer
          minimum: 0
          maximum: 100
        message:
          type: string
        steps:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              completed:
                type: boolean
              active:
                type: boolean
        result:
          type:
            - object
            - 'null'
          additionalProperties: true
          description: >
            Result payload when `status` is `completed`. Shape depends on
            `type`:


            - `blog_post.generate` → `{ post_id, slug, title }`

            - `social_post.generate` → `{ social_post_id, title, platforms }`

            - `image.generate` → `{ url, key, width, height }`

            - `translation.batch` → `{ keys_translated: int, locales: [string]
            }`
        error:
          type:
            - object
            - 'null'
          properties:
            code:
              type: string
              example: upstream_unavailable
            message:
              type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    Category:
      type: object
      required:
        - id
        - name
        - slug
      properties:
        id:
          type: integer
          example: 7
        name:
          type: string
          example: Wellness
        slug:
          type: string
          example: wellness
    Tag:
      type: object
      required:
        - id
        - name
        - slug
      properties:
        id:
          type: integer
          example: 21
        name:
          type: string
          example: breathwork
        slug:
          type: string
          example: breathwork
    BlogPostTranslation:
      type: object
      required:
        - lang
        - title
        - content
      properties:
        lang:
          type: string
          example: en
        title:
          type: string
        excerpt:
          type:
            - string
            - 'null'
        content:
          type: string
          description: HTML.
        meta_title:
          type:
            - string
            - 'null'
        meta_description:
          type:
            - string
            - 'null'
    JobType:
      type: string
      enum:
        - blog_post.generate
        - social_post.generate
        - image.generate
        - translation.batch
        - content_plan.generate
        - website.generate
    JobStatus:
      type: string
      enum:
        - pending
        - processing
        - completed
        - failed
        - cancelled
    Error:
      type: object
      description: |
        RFC 7807 problem+json error. The `code` is a stable machine
        identifier; clients should branch on `code`, never on `title`.
      required:
        - type
        - title
        - status
        - code
      properties:
        type:
          type: string
          format: uri
          example: https://api.neuraldraft.io/errors/validation_failed
        title:
          type: string
          example: Validation failed
        status:
          type: integer
          example: 422
        detail:
          type: string
          example: One or more fields failed validation.
        instance:
          type: string
          description: Request id (also returned as `X-Request-Id` header).
          example: req_2Nh4PqRsTuVw
        code:
          type: string
          description: |
            Stable machine error code. Standard values:
            `bad_request`, `unauthorized`, `insufficient_credits`,
            `forbidden`, `not_found`, `conflict`, `validation_failed`,
            `rate_limited`, `internal_error`, `service_unavailable`,
            `slot_unavailable`, `connect_not_ready`, `upstream_unavailable`,
            `idempotency_conflict`.
          example: validation_failed
        cost:
          type: integer
          description: |
            Credits the operation requires. Only present on
            `402 insufficient_credits` responses.
          example: 1
        balance:
          type: integer
          description: |
            Credits the project had on hand at the time of the request.
            Only present on `402 insufficient_credits` responses.
          example: 0
        errors:
          type: object
          description: Field-level validation errors (only on 422).
          additionalProperties:
            type: array
            items:
              type: string
          example:
            customer_email:
              - The customer email field must be a valid email.
            starts_at:
              - The starts at field must be a valid ISO 8601 date.
  responses:
    BadRequest:
      description: Malformed request — missing required field, invalid JSON, etc.
      headers:
        X-Request-Id:
          schema:
            type: string
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            bad:
              value:
                type: https://api.neuraldraft.io/errors/bad_request
                title: Bad request
                status: 400
                detail: Request body could not be parsed.
                code: bad_request
                instance: req_2Nh4PqRsTuVw
    Unauthorized:
      description: Missing or invalid API key.
      headers:
        WWW-Authenticate:
          schema:
            type: string
          description: '`Bearer realm="Neural Draft"`.'
        X-Request-Id:
          schema:
            type: string
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            missing:
              value:
                type: https://api.neuraldraft.io/errors/unauthorized
                title: Unauthorized
                status: 401
                detail: API key missing or invalid.
                code: unauthorized
                instance: req_2Nh4PqRsTuVw
    PaymentRequired:
      description: |
        Insufficient credits. The project does not have enough credits to
        cover the operation. The body includes `cost` (credits the operation
        needs) and `balance` (credits the project had on hand) so clients
        can render an exact top-up prompt. Top up at `/projects/me/usage`
        or wait for the next period to start.
      headers:
        X-Request-Id:
          schema:
            type: string
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            broke:
              value:
                type: https://neuraldraft.com/errors/payment-required
                title: Payment Required
                status: 402
                detail: This project has no credits remaining for the current period.
                code: insufficient_credits
                cost: 1
                balance: 0
                instance: req_2Nh4PqRsTuVw
    Forbidden:
      description: API key lacks required scope.
      headers:
        X-Request-Id:
          schema:
            type: string
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            scope:
              value:
                type: https://api.neuraldraft.io/errors/forbidden
                title: Forbidden
                status: 403
                detail: This API key does not have the `commerce:write` scope.
                code: forbidden
                instance: req_2Nh4PqRsTuVw
    Conflict:
      description: Conflicting state — duplicate slug, race condition, etc.
      headers:
        X-Request-Id:
          schema:
            type: string
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            slug:
              value:
                type: https://api.neuraldraft.io/errors/conflict
                title: Conflict
                status: 409
                detail: A resource with this slug already exists.
                code: conflict
                instance: req_2Nh4PqRsTuVw
    UnprocessableEntity:
      description: Semantic validation failed (well-formed body, but field values invalid).
      headers:
        X-Request-Id:
          schema:
            type: string
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            invalid:
              value:
                type: https://api.neuraldraft.io/errors/validation_failed
                title: Validation failed
                status: 422
                detail: One or more fields failed validation.
                code: validation_failed
                instance: req_2Nh4PqRsTuVw
                errors:
                  customer_email:
                    - The customer email field must be a valid email address.
                  starts_at:
                    - The starts at field must be a date after now.
  securitySchemes:
    apiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: |
        Project API key. Pass as `Authorization: Bearer ndsk_live_...`. Manage
        keys via [`/projects/me/api-keys`](#tag/Projects). Test-mode keys use
        the `ndsk_test_` prefix.

````