ClassQuill Blog API

The Blog API from ClassQuill — 3 operation(s) for blog.

Operations 6

POST /v1/blog-posts Create Blog Post #
GET /v1/blog-posts List Blog Posts #
PATCH /v1/blog-posts/{post_id} Update Blog Post #
DELETE /v1/blog-posts/{post_id} Delete Blog Post #
GET /v1/blog-posts/{post_id} Get Blog Post #
POST /v1/blog-images Upload Blog Image #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/classquill-com:classquill-com-blog-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

classquill-com-blog-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: ClassQuill Public Blog API
  description: 'REST API for ClassQuill tutoring organisations. Pipe your sessions, invoices, payments, tutor earnings, students, and more into your own tools. Most endpoints are reads; a focused set of writes (each gated by a *:write scope) create or update data, including creating student/tutor/parent accounts.


    Authenticate every request with an org API key:


    Authorization: Token token=cq_live_...


    Mint keys in the ClassQuill app under Settings → Developers.'
  version: 1.0.0
security:
- ApiKeyAuth: []
tags:
- name: Blog
paths:
  /v1/blog-posts:
    post:
      tags:
      - Blog
      summary: Create Blog Post
      description: 'Create a blog post on the calling key''s OWN organisation blog — the post is

        scoped to the key''s org, so callers normally send only `title` (+ optional fields).

        Defaults `status=''draft''`. Slug is derived from the title when omitted and must be

        unique per org. Honours `Idempotency-Key`. Requires the `blog:write` scope (NOT

        granted by the `write` umbrella).


        `target`/`brand` are reserved for EquateIt marketing sites: `target=''platform''`

        writes a public marketing post (organization_id NULL), requires the key''s

        platform-blog capability, and `brand` (required for platform) selects the surface.'
      operationId: create_blog_post_v1_blog_posts_post
      parameters:
      - name: Idempotency-Key
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Idempotency-Key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BlogPostCreate'
      responses:
        '201':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BlogPost'
        '422':
          description: Validation error — `error.details.errors` lists the fields.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '401':
          description: Missing, malformed, revoked, or expired credentials.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '403':
          description: The credential lacks the scope this endpoint requires.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '429':
          description: Rate limit exceeded — honour Retry-After and the RateLimit-* headers.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
    get:
      tags:
      - Blog
      summary: List Blog Posts
      description: 'List blog posts visible to the key: its OWN org''s posts always, plus platform

        posts when the key has the platform-blog capability. Optional brand/status/target

        filters. Includes drafts (this is an authoring surface). Requires `read`.'
      operationId: list_blog_posts_v1_blog_posts_get
      parameters:
      - name: brand
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Filter by brand (equateit | classquill).
          title: Brand
        description: Filter by brand (equateit | classquill).
      - name: status
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Filter by status (draft | published).
          title: Status
        description: Filter by status (draft | published).
      - name: target
        in: query
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Filter by target (platform | org).
          title: Target
        description: Filter by target (platform | org).
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          maximum: 200
          minimum: 1
          description: Records per page (default 50, max 200)
          default: 50
          title: Limit
        description: Records per page (default 50, max 200)
      - name: offset
        in: query
        required: false
        schema:
          type: integer
          minimum: 0
          description: Pagination offset
          default: 0
          title: Offset
        description: Pagination offset
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/BlogPost'
                title: Response List Blog Posts V1 Blog Posts Get
        '422':
          description: Validation error — `error.details.errors` lists the fields.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '401':
          description: Missing, malformed, revoked, or expired credentials.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '403':
          description: The credential lacks the scope this endpoint requires.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '429':
          description: Rate limit exceeded — honour Retry-After and the RateLimit-* headers.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
  /v1/blog-posts/{post_id}:
    patch:
      tags:
      - Blog
      summary: Update Blog Post
      description: 'Update fields and/or flip `status` to ''published''. brand/target are immutable.

        Re-publishing an already-published post is a no-op success (no duplicate rebuild

        dispatch). Cross-scope id → 404. Requires `blog:write`.'
      operationId: update_blog_post_v1_blog_posts__post_id__patch
      parameters:
      - name: post_id
        in: path
        required: true
        schema:
          type: string
          title: Post Id
      - name: Idempotency-Key
        in: header
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          title: Idempotency-Key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BlogPostUpdate'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BlogPost'
        '422':
          description: Validation error — `error.details.errors` lists the fields.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '401':
          description: Missing, malformed, revoked, or expired credentials.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '403':
          description: The credential lacks the scope this endpoint requires.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '429':
          description: Rate limit exceeded — honour Retry-After and the RateLimit-* headers.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '404':
          description: Not found — the record doesn't exist or belongs to another organisation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
    delete:
      tags:
      - Blog
      summary: Delete Blog Post
      description: 'Delete a blog post. 204 on success, 404 if not found or out of the key''s scope

        (own org, or platform when the key is platform-capable). Published posts can be

        deleted (no guard — PATCH to unpublish first if preferred). DELETE is already

        idempotent, so it takes no Idempotency-Key. Requires `blog:write`.'
      operationId: delete_blog_post_v1_blog_posts__post_id__delete
      parameters:
      - name: post_id
        in: path
        required: true
        schema:
          type: string
          title: Post Id
      responses:
        '204':
          description: Successful Response
        '422':
          description: Validation error — `error.details.errors` lists the fields.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '401':
          description: Missing, malformed, revoked, or expired credentials.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '403':
          description: The credential lacks the scope this endpoint requires.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '429':
          description: Rate limit exceeded — honour Retry-After and the RateLimit-* headers.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '404':
          description: Not found — the record doesn't exist or belongs to another organisation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
    get:
      tags:
      - Blog
      summary: Get Blog Post
      description: 'Fetch one blog post the key may see (own org, or platform when capable).

        Cross-scope / missing → 404. Requires `read`.'
      operationId: get_blog_post_v1_blog_posts__post_id__get
      parameters:
      - name: post_id
        in: path
        required: true
        schema:
          type: string
          description: Blog post UUID
          title: Post Id
        description: Blog post UUID
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BlogPost'
        '422':
          description: Validation error — `error.details.errors` lists the fields.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '401':
          description: Missing, malformed, revoked, or expired credentials.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '403':
          description: The credential lacks the scope this endpoint requires.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '429':
          description: Rate limit exceeded — honour Retry-After and the RateLimit-* headers.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '404':
          description: Not found — the record doesn't exist or belongs to another organisation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
  /v1/blog-images:
    post:
      tags:
      - Blog
      summary: Upload Blog Image
      description: 'Upload an image to the `blog-images` Supabase storage bucket and return its public

        URL (use as a post''s `featured_image` or inline in `content`). By default the image

        is stored under the calling key''s OWN organisation.


        * target=org (default) → `org//-` (the key''s own org).

        * target=platform → `platform//-`; reserved for EquateIt

        marketing sites — requires the key''s platform-blog capability.


        415 on an unsupported MIME type, 413 over 10 MB. Requires `blog:write`.'
      operationId: upload_blog_image_v1_blog_images_post
      parameters:
      - name: target
        in: query
        required: false
        schema:
          type: string
          description: org (default) | platform
          default: org
          title: Target
        description: org (default) | platform
      - name: brand
        in: query
        required: false
        schema:
          type: string
          description: equateit | classquill (platform only)
          default: classquill
          title: Brand
        description: equateit | classquill (platform only)
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/Body_upload_blog_image_v1_blog_images_post'
      responses:
        '201':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BlogImageUploadResult'
        '422':
          description: Validation error — `error.details.errors` lists the fields.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '401':
          description: Missing, malformed, revoked, or expired credentials.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '403':
          description: The credential lacks the scope this endpoint requires.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
        '429':
          description: Rate limit exceeded — honour Retry-After and the RateLimit-* headers.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
components:
  schemas:
    BlogImageUploadResult:
      properties:
        url:
          type: string
          title: Url
        path:
          type: string
          title: Path
        size_bytes:
          type: integer
          title: Size Bytes
      type: object
      required:
      - url
      - path
      - size_bytes
      title: BlogImageUploadResult
      description: 'POST /v1/blog-images result — the public URL of the uploaded image (use as a

        post''s `featured_image` or inline in `content`), the storage path it was written

        to inside the `blog-images` bucket, and its size in bytes.'
    ApiErrorBody:
      description: The `error` object every /v1 error response carries.
      properties:
        code:
          description: 'Stable machine-readable error code: bad_request, unauthorized, forbidden, not_found, conflict, validation_error, rate_limited, or internal_error.'
          examples:
          - not_found
          title: Code
          type: string
        message:
          description: Human-readable explanation, safe to surface to end users.
          examples:
          - Session not found
          title: Message
          type: string
        status:
          description: The HTTP status code, repeated in the body.
          examples:
          - 404
          title: Status
          type: integer
        details:
          additionalProperties: true
          description: Error-specific context. 422 carries the field errors under `errors`; 429 carries `retry_after_seconds`.
          title: Details
          type: object
      required:
      - code
      - message
      - status
      title: ApiErrorBody
      type: object
    ApiError:
      description: Canonical error envelope for every /v1 error response (4xx/5xx).
      properties:
        error:
          $ref: '#/components/schemas/ApiErrorBody'
      required:
      - error
      title: ApiError
      type: object
    BlogPostCreate:
      properties:
        title:
          type: string
          maxLength: 300
          minLength: 1
          title: Title
        slug:
          anyOf:
          - type: string
            maxLength: 200
            pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
          - type: 'null'
          title: Slug
          description: URL slug; slugified from the title when omitted. Unique per org.
        excerpt:
          anyOf:
          - type: string
            maxLength: 2000
          - type: 'null'
          title: Excerpt
        content:
          anyOf:
          - type: string
            maxLength: 200000
          - type: 'null'
          title: Content
          description: Markdown body.
        tags:
          items:
            type: string
          type: array
          maxItems: 50
          title: Tags
        featured_image:
          anyOf:
          - type: string
            maxLength: 2000
          - type: 'null'
          title: Featured Image
        author:
          anyOf:
          - type: string
            maxLength: 200
          - type: 'null'
          title: Author
        category:
          anyOf:
          - type: string
            maxLength: 200
          - type: 'null'
          title: Category
        date:
          anyOf:
          - type: string
          - type: 'null'
          title: Date
          description: ISO date; defaults to today.
        status:
          type: string
          enum:
          - draft
          - published
          title: Status
          default: draft
        publish_to_website:
          anyOf:
          - type: boolean
          - type: 'null'
          title: Publish To Website
        publish_to_portal:
          anyOf:
          - type: boolean
          - type: 'null'
          title: Publish To Portal
        target:
          type: string
          enum:
          - platform
          - org
          title: Target
          description: Leave as 'org' (the default) to post to your own organisation's blog. 'platform' targets an EquateIt-operated marketing site and requires the key's platform-blog capability.
          default: org
        brand:
          anyOf:
          - type: string
            enum:
            - equateit
            - classquill
          - type: 'null'
          title: Brand
          description: Only for platform posts — which marketing surface (equateit → equateit.com.au, classquill → classquill.com). Omit for org posts.
        scheduled_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Scheduled At
          description: ISO 8601 datetime (e.g. '2026-08-01T09:00:00Z'). When set with status='draft', the post will be published automatically by a background job within 15 minutes of this time. Pass null to cancel a scheduled publish.
      additionalProperties: false
      type: object
      required:
      - title
      title: BlogPostCreate
      description: 'POST /v1/blog-posts → blog_posts. Creates a post on the calling key''s OWN

        organisation blog — the post is scoped to the org the API key belongs to, so most

        callers send only `title` (+ `content` and other optional fields) and nothing else.

        Defaults status=''draft'' (never auto-publish an unreviewed post). organization_id /

        created_by / timestamps are server-set, never accepted.


        `target` / `brand` are reserved for EquateIt-operated marketing sites and are NOT

        needed for normal org publishing: `target=''platform''` writes to a public marketing

        blog and requires the key''s `allow_platform_blog` capability (not granted to ordinary

        org keys), with `brand` selecting the surface.'
    BlogPost:
      properties:
        id:
          type: string
          title: Id
        target:
          anyOf:
          - type: string
          - type: 'null'
          title: Target
        brand:
          anyOf:
          - type: string
          - type: 'null'
          title: Brand
        organization_id:
          anyOf:
          - type: string
          - type: 'null'
          title: Organization Id
        title:
          anyOf:
          - type: string
          - type: 'null'
          title: Title
        slug:
          anyOf:
          - type: string
          - type: 'null'
          title: Slug
        status:
          anyOf:
          - type: string
          - type: 'null'
          title: Status
        excerpt:
          anyOf:
          - type: string
          - type: 'null'
          title: Excerpt
        content:
          anyOf:
          - type: string
          - type: 'null'
          title: Content
        author:
          anyOf:
          - type: string
          - type: 'null'
          title: Author
        tags:
          items:
            type: string
          type: array
          title: Tags
          default: []
        featured_image:
          anyOf:
          - type: string
          - type: 'null'
          title: Featured Image
        category:
          anyOf:
          - type: string
          - type: 'null'
          title: Category
        date:
          anyOf:
          - type: string
          - type: 'null'
          title: Date
        publish_to_website:
          anyOf:
          - type: boolean
          - type: 'null'
          title: Publish To Website
        publish_to_portal:
          anyOf:
          - type: boolean
          - type: 'null'
          title: Publish To Portal
        is_pinned:
          anyOf:
          - type: boolean
          - type: 'null'
          title: Is Pinned
        published_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Published At
        scheduled_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Scheduled At
        created_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Created At
        updated_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Updated At
      type: object
      required:
      - id
      title: BlogPost
      description: 'A blog post row returned by /v1/blog-posts. `target` is derived from

        organization_id (NULL → ''platform'', else ''org'').'
    BlogPostUpdate:
      properties:
        title:
          anyOf:
          - type: string
            maxLength: 300
            minLength: 1
          - type: 'null'
          title: Title
        slug:
          anyOf:
          - type: string
            maxLength: 200
            pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
          - type: 'null'
          title: Slug
        excerpt:
          anyOf:
          - type: string
            maxLength: 2000
          - type: 'null'
          title: Excerpt
        content:
          anyOf:
          - type: string
            maxLength: 200000
          - type: 'null'
          title: Content
        tags:
          anyOf:
          - items:
              type: string
            type: array
            maxItems: 50
          - type: 'null'
          title: Tags
        featured_image:
          anyOf:
          - type: string
            maxLength: 2000
          - type: 'null'
          title: Featured Image
        author:
          anyOf:
          - type: string
            maxLength: 200
          - type: 'null'
          title: Author
        category:
          anyOf:
          - type: string
            maxLength: 200
          - type: 'null'
          title: Category
        date:
          anyOf:
          - type: string
          - type: 'null'
          title: Date
        status:
          anyOf:
          - type: string
            enum:
            - draft
            - published
          - type: 'null'
          title: Status
        publish_to_website:
          anyOf:
          - type: boolean
          - type: 'null'
          title: Publish To Website
        publish_to_portal:
          anyOf:
          - type: boolean
          - type: 'null'
          title: Publish To Portal
        scheduled_at:
          anyOf:
          - type: string
          - type: 'null'
          title: Scheduled At
          description: ISO 8601 datetime. Set to schedule deferred publishing; set to null to cancel a scheduled publish.
      additionalProperties: false
      type: object
      title: BlogPostUpdate
      description: 'PATCH /v1/blog-posts/{id} → blog_posts. Partial update; only these fields are

        writable. Flip `status` to ''published'' to publish (idempotent — re-publishing is a

        no-op). brand/target/organization_id are immutable after creation (not accepted);

        updated_at is trigger-maintained.'
    Body_upload_blog_image_v1_blog_images_post:
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          title: File
      type: object
      required:
      - file
      title: Body_upload_blog_image_v1_blog_images_post
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: Authorization
      description: Org API key as `Token token=cq_live_...`