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: 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_...`