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

# Import a post from Markdown or MDX

> Creates a blog post from a Markdown or MDX document with front matter. Send JSON, the raw document (`text/markdown`, at most 5 MB by default), or a multipart upload with the file and its images.

Images: `featuredImage` in the front matter and every image in the body may be an `https://` URL (fetched, at most 15 MB by default), a library image’s slug, a filename posted with the file (`image`, up to 10), or `generate: <description>` with `generateImages` (at most 4 per import).

**Media on an API key:** a new image (a URL, a posted file or `generate:`) needs `content:images:write`, a new video `content:videos:write` and `content:videos:publish`; a library slug needs `content:images:read` / `content:videos:read`. The blog scopes don’t cover media, and the refusal names the missing scope.

**Publishing:** `publish=true` publishes only when the caller may (a key needs `content:blog:publish`); otherwise the post is saved as a draft. An MDX import is always a draft.

**Removing an import:** delete the post by the draft `_id` the import answered. On an API key that needs `content:blog:delete`.

**Access:** an API key with `content:blog:write`, or `content:blog:all`, or `content:all`, or `admin:all`, or a signed-in user holding the `blog:import-markdown` permission. Other callers get 403.

**Rate limit:** the key’s own writes-per-minute limit, set when the key is created (default 120), and 30 imports and validations a minute per caller: whichever runs out first answers 429 with `Retry-After`.



## OpenAPI

````yaml /api-reference/openapi.yaml post /@apostrophecms/blog/from-markdown
openapi: 3.1.0
info:
  title: Fetchski Attract CMS API
  description: >-
    Auto-generated OpenAPI spec for this Fetchski site instance. Authenticate
    machine-to-machine requests with an API key by sending the header
    `Authorization: ApiKey <token>`. Keys are issued in the CMS (API Keys) and
    carry scopes that limit them to specific actions. Each key also has its own
    security policy, chosen when it is created: a signing mode (token only,
    signed writes, or every request signed with the key’s signing secret via
    `X-Fetchski-Timestamp` + `X-Fetchski-Signature`), an optional network
    allowlist, and reads/writes-per-minute limits (429 with `Retry-After` over
    them). Some routes require a signature from every key — see each route.
  version: 1.0.0
servers:
  - url: https://{host}/api/v1
    description: 'Your site: set `host` to its host name.'
    variables:
      host:
        default: acme.example
security: []
tags:
  - name: Authentication
    description: >-
      Sign in as a person to get a bearer token or a session cookie.
      Integrations use an API key instead: send `Authorization: ApiKey <token>`.
  - name: SDK Packages
    description: >-
      The site’s own npm registry and Composer repository for the Attract SDKs,
      so an app installs them with one of the site’s API keys. Point npm, pnpm,
      Bun or Composer at them; the SDKs never call them.
  - name: Pages
    description: 'The page tree: each page’s content, place and URL.'
  - name: Global Content
    description: Site-wide content and settings every page shares.
  - name: Redirects
    description: Old URLs and where they now send visitors.
  - name: Internationalization
    description: The site’s locales.
  - name: Languages
    description: >-
      The site’s languages: the main language’s name and every further
      language’s name, code and Public switch. A save applies at once: a new
      language gets its home page and its own addresses (/fr/…), and its pages
      are then translated with each type’s localize routes.
  - name: Page Templates
    description: >-
      The site’s whole-page starting arrangements. They are not pages of the
      site (the Pages list leaves them out for API keys): read one, then make a
      page from it with Pages → create and `_copyingId` (`copyingId` in the
      SDKs).
  - name: Blog Posts
    description: The blog’s posts, and importing them from Markdown or MDX.
  - name: Blog Categories
    description: The categories posts are filed under.
  - name: Authors
    description: The people posts are credited to.
  - name: Team Members
    description: The people the Team section shows.
  - name: Testimonials
    description: Quotes from customers.
  - name: FAQ Items
    description: Questions and their answers.
  - name: FAQ Categories
    description: The groups questions are sorted into.
  - name: Google Reviews
    description: Reviews from the business’s Google profile, synced on a schedule.
  - name: Instagram Posts
    description: Posts from the connected Instagram account, synced on a schedule.
  - name: Service Areas
    description: The places the business serves, for the service map.
  - name: Cookies
    description: The cookies the consent banner declares.
  - name: Media
    description: The Images and Files libraries, and their tags.
  - name: Videos
    description: The Videos library.
  - name: Image Folders
    description: The folders that organize the Images library.
  - name: File Folders
    description: The folders that organize the Files library.
  - name: Video Folders
    description: The folders that organize the Videos library.
  - name: Photo Albums
    description: Hand-picked, ordered sets of photos for galleries.
  - name: Photo Feeds
    description: >-
      Feeds that collect photos from a connected photo source into the Images
      library on a schedule.
  - name: Attachments
    description: Uploading files and cropping images.
  - name: Fetchski Media
    description: >-
      Read-only discovery over the Images and Videos libraries: find what to
      reference in an MDX blog import, or which file to use, with its URLs.
  - name: Forms
    description: The site’s forms and their fields.
  - name: Form Submissions
    description: >-
      What visitors sent through the site’s forms. Submissions hold personal
      details: give a key `forms:read` only where it is needed.
  - name: Conversion Events
    description: >-
      The conversion event log — one record per conversion, with its attribution
      and consent snapshot and its delivery to each ad platform. Read it
      incrementally, post facts from other systems (a reservation purchase, a
      lead outcome), and adjust them (refunds, cancellations). One event ID
      travels everywhere as the platforms’ de-duplication key.
  - name: Visitor Attribution
    description: >-
      Per-visitor attribution on this site: first touch, last ad or campaign
      touch, and the ad platforms’ browser IDs. The join other systems use to
      attach a purchase to the marketing-site journey that led to it.
  - name: Ads Metrics
    description: >-
      Aggregates for dashboards: ad-click landings and conversions, by day,
      platform, campaign and more.
  - name: Conversions
    description: >-
      Conversion definitions — the visitor actions (a lead, a call click, a
      booking, a purchase) this site reports to the ad platforms, each with a
      row per platform. Managed by administrators in the CMS.
  - name: Webhooks
    description: >-
      Push notifications of form submissions and conversion-event changes to the
      site’s Webhook Destinations — signed with each destination’s own secret,
      retried for a day, at-least-once. Set destinations up in the CMS (Webhook
      Destinations, for people granted “Manage webhook destinations”): URL,
      events, optional filters and custom headers, and the signing secret shown
      once.
  - name: Request Log
    description: >-
      The page views the site captured (Connections → Tracking → Request log),
      and the query parameters on them: campaign tags and ad click IDs.
paths:
  /@apostrophecms/blog/from-markdown:
    post:
      tags:
        - Blog Posts
      summary: Import a post from Markdown or MDX
      description: >-
        Creates a blog post from a Markdown or MDX document with front matter.
        Send JSON, the raw document (`text/markdown`, at most 5 MB by default),
        or a multipart upload with the file and its images.


        Images: `featuredImage` in the front matter and every image in the body
        may be an `https://` URL (fetched, at most 15 MB by default), a library
        image’s slug, a filename posted with the file (`image`, up to 10), or
        `generate: <description>` with `generateImages` (at most 4 per import).


        **Media on an API key:** a new image (a URL, a posted file or
        `generate:`) needs `content:images:write`, a new video
        `content:videos:write` and `content:videos:publish`; a library slug
        needs `content:images:read` / `content:videos:read`. The blog scopes
        don’t cover media, and the refusal names the missing scope.


        **Publishing:** `publish=true` publishes only when the caller may (a key
        needs `content:blog:publish`); otherwise the post is saved as a draft.
        An MDX import is always a draft.


        **Removing an import:** delete the post by the draft `_id` the import
        answered. On an API key that needs `content:blog:delete`.


        **Access:** an API key with `content:blog:write`, or `content:blog:all`,
        or `content:all`, or `admin:all`, or a signed-in user holding the
        `blog:import-markdown` permission. Other callers get 403.


        **Rate limit:** the key’s own writes-per-minute limit, set when the key
        is created (default 120), and 30 imports and validations a minute per
        caller: whichever runs out first answers 429 with `Retry-After`.
      operationId: importBlogPost
      parameters:
        - name: format
          in: query
          required: false
          description: Read a raw or JSON body as `mdx`.
          schema:
            type: string
            enum:
              - md
              - mdx
        - name: publish
          in: query
          required: false
          description: The same as the body’s `publish`.
          schema:
            type: boolean
        - name: generateImages
          in: query
          required: false
          description: The same as the body’s `generateImages`.
          schema:
            type: boolean
        - name: _ai
          in: query
          required: false
          description: The same as the body’s `_ai`.
          schema:
            type: boolean
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BlogImportRequest'
          text/markdown:
            schema:
              type: string
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/BlogImportUpload'
      responses:
        '200':
          description: The new post.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BlogImportResult'
        '400':
          $ref: '#/components/responses/KeyedInvalid'
        '401':
          $ref: '#/components/responses/KeyedUnauthorized'
        '403':
          $ref: '#/components/responses/KeyedForbidden'
        '413':
          description: >-
            The document or a file is over its size limit (the message names
            it).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KeyedError'
              example:
                name: invalid
                message: An uploaded file is too large (max 15 MB per file).
        '429':
          $ref: '#/components/responses/KeyedRateLimited'
      security:
        - ApiKeyAuth:
            - content:blog:write
      x-codeSamples:
        - lang: typescript
          label: TypeScript
          source: >-
            import { createSiteClient } from '@fetchski/attract-cms-sdk';


            const site = createSiteClient();

            const blogImportResult = await site.blogPosts().importBlogPost({
            markdown: '---\ntitle: "Our spring update"\nexcerpt: "What changed
            this season."\n---\n\n## What’s new\n\nThe body, in Markdown.\n' });
        - lang: php
          label: Laravel
          source: >-
            use Fetchski\AttractCmsLaravel\Facades\Attract;


            $blogImportResult = Attract::blogPosts()->importBlogPost(markdown:
            '---

            title: "Our spring update"

            excerpt: "What changed this season."

            ---


            ## What’s new


            The body, in Markdown.

            ');
components:
  schemas:
    BlogImportRequest:
      type: object
      required:
        - markdown
      properties:
        markdown:
          type: string
          description: >-
            The whole document: YAML front matter between `---` lines, then the
            Markdown body. `title` is the one required front matter key; `slug`,
            `publishedAt`, `authorName` (or `authorEmail`), `category`,
            `excerpt`, `featuredImage` (+ `featuredImageAlt`), `seoTitle`,
            `seoDescription` and `splitDepth` are optional (the Markdown
            template, `getBlogMarkdownTemplate`, names every key). Body headings
            start at `##`: the title is the post’s only `<h1>`.
          example: |
            ---
            title: "Our spring update"
            excerpt: "What changed this season."
            ---

            ## What’s new

            The body, in Markdown.
        format:
          type: string
          enum:
            - md
            - mdx
          default: md
          description: 'How to read `markdown`: plain Markdown (`md`) or MDX (`mdx`).'
        publish:
          type: boolean
          default: false
          description: >-
            Publish the post when the caller may; otherwise (and by default) it
            is saved as a draft.
        generateImages:
          type: boolean
          default: false
          description: >-
            Allow `generate:` image references (needs
            `content:blog:generate-image` on an API key). An import that
            generates images must finish them within 600 seconds, or it stops
            and leaves nothing behind (422 `deadline`).
        _ai:
          type: boolean
          default: false
          description: >-
            Mark the post (and its generated images) as AI-written in its
            version history.
    BlogImportUpload:
      type: object
      required:
        - file
      properties:
        file:
          type: string
          format: binary
          description: The `.md` or `.mdx` file (`.mdx` is read as MDX).
        image:
          type: array
          maxItems: 10
          items:
            type: string
            format: binary
          description: Images the document references by filename.
        format:
          type: string
          enum:
            - md
            - mdx
        publish:
          type: boolean
        generateImages:
          type: boolean
        _ai:
          type: boolean
    BlogImportResult:
      type: object
      required:
        - _id
        - type
        - title
        - slug
        - published
      properties:
        _id:
          type: string
        type:
          type: string
          example: '@apostrophecms/blog'
        title:
          type: string
        slug:
          type: string
        _url:
          type: string
          description: The post’s page.
        published:
          type: boolean
          description: False when it was saved as a draft.
      example:
        _id: clx8k2m0a0011abcd1234efgh:en:draft
        type: '@apostrophecms/blog'
        title: Five things we learned this year
        slug: five-things-we-learned-this-year
        _url: https://www.example.com/blog/five-things-we-learned-this-year
        published: false
    KeyedError:
      type: object
      description: >-
        Every refusal: the error kind, a message saying what to fix, and
        details.
      required:
        - name
        - message
      properties:
        name:
          type: string
          description: The error kind, e.g. `forbidden`.
        message:
          type: string
        data:
          type: object
          additionalProperties: true
  responses:
    KeyedInvalid:
      description: The request is not valid; the message says which part and how to fix it.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/KeyedError'
          example:
            name: invalid
            message: Send the Markdown as `markdown` (a string).
            data: {}
    KeyedUnauthorized:
      description: >-
        The API key is missing, malformed, unknown, disabled, expired or
        archived, or a request its signing mode requires to be signed is not.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/KeyedError'
          example:
            name: invalid
            message: The API key is not valid.
            data: {}
    KeyedForbidden:
      description: >-
        The API key lacks the scope this route needs (the message names it) or
        is used from outside its allowlist; or, on a route that answers 403 to
        everyone it refuses, the caller may not use it.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/KeyedError'
          example:
            name: forbidden
            message: 'This API key lacks the required scope: forms:read.'
            data: {}
    KeyedRateLimited:
      description: The key used up its per-minute limit. Wait `Retry-After` seconds.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/KeyedError'
          example:
            name: tooManyRequests
            message: This API key’s rate limit is used up for this minute.
            data: {}
      headers:
        Retry-After:
          description: Seconds until the budget resets.
          schema:
            type: integer
            minimum: 1
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: Authorization
      description: >-
        Paste your API key as the CMS showed it: `fsk_<site>_<key id>_<secret>`
        (issued under API Keys). `ApiKey ` in front of it works too. Leave it
        empty to read as the public.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.