{
  "openapi": "3.0.3",
  "info": {
    "title": "CrediBlog JSON API",
    "version": "1.0.0",
    "description": "Read the posts, categories, tags, planned topics, and site profile of one CrediBlog site.\n\nCrediBlog researches, writes, and stores your content; this API is how you render it yourself — in Next.js, Astro, WordPress, a mobile app, or anything else that can make an HTTP request.\n\nAuthenticate with an API key in the `X-Api-Key` header. Keys are minted per site in the dashboard and are shown exactly once. Every response is scoped to the site the key belongs to; a key can never read another site's content.\n\nCall this API from a server. The key is a secret and browsers cannot keep one.",
    "contact": { "name": "CrediBlog support", "email": "support@crediblog.com", "url": "https://crediblog.com/docs" },
    "license": { "name": "Proprietary", "url": "https://crediblog.com/terms" }
  },
  "servers": [
    { "url": "https://crediblog.com", "description": "Production" }
  ],
  "security": [{ "ApiKeyAuth": [] }],
  "tags": [
    { "name": "Posts", "description": "Published and draft articles." },
    { "name": "Taxonomy", "description": "Categories and tags." },
    { "name": "Planning", "description": "The content calendar." },
    { "name": "Site", "description": "Branding, author, and blog profile." },
    { "name": "Meta", "description": "Discovery and the machine-readable contract." }
  ],
  "paths": {
    "/api/v1": {
      "get": {
        "tags": ["Meta"],
        "operationId": "getIndex",
        "summary": "Discovery document",
        "description": "Resource URLs, the scopes on the calling key, and the rate limit that applies to it.",
        "responses": {
          "200": {
            "description": "Index",
            "headers": { "ETag": { "$ref": "#/components/headers/ETag" } },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Index" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/openapi.json": {
      "get": {
        "tags": ["Meta"],
        "operationId": "getOpenApi",
        "summary": "This document",
        "description": "Public. No API key required, so you can generate a client before you have a key.",
        "security": [],
        "responses": {
          "200": { "description": "The OpenAPI document", "content": { "application/json": {} } }
        }
      }
    },
    "/api/v1/site": {
      "get": {
        "tags": ["Site"],
        "operationId": "getSite",
        "summary": "Get the site profile",
        "description": "Name, tagline, description, locale, author byline, and public URL — everything needed to render your own header and footer.",
        "responses": {
          "200": {
            "description": "The site",
            "headers": {
              "ETag": { "$ref": "#/components/headers/ETag" },
              "Cache-Control": { "$ref": "#/components/headers/CacheControl" }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Site" } } }
          },
          "304": { "$ref": "#/components/responses/NotModified" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/posts": {
      "get": {
        "tags": ["Posts"],
        "operationId": "listPosts",
        "summary": "List posts",
        "description": "Newest first by default. Article bodies (`contentHtml`) and citations (`sources`) are omitted from list responses unless you ask for them with `fields`.",
        "parameters": [
          { "$ref": "#/components/parameters/limit" },
          { "$ref": "#/components/parameters/cursor" },
          {
            "name": "sort", "in": "query", "required": false,
            "description": "Field to order by. Drafts have no publication date, so under `publishedAt` they order by when they were created.",
            "schema": { "type": "string", "enum": ["publishedAt", "createdAt", "title", "views"], "default": "publishedAt" }
          },
          { "$ref": "#/components/parameters/order" },
          {
            "name": "fields", "in": "query", "required": false,
            "description": "Comma-separated subset of post fields. `id` is always included.",
            "schema": { "type": "string" },
            "example": "id,slug,title,excerpt,publishedAt,coverImageUrl"
          },
          {
            "name": "status", "in": "query", "required": false,
            "schema": { "type": "string", "enum": ["draft", "published"] }
          },
          { "name": "category", "in": "query", "required": false, "description": "Category slug.", "schema": { "type": "string" } },
          { "name": "tag", "in": "query", "required": false, "description": "Tag slug.", "schema": { "type": "string" } },
          {
            "name": "publishedFrom", "in": "query", "required": false,
            "description": "Inclusive lower bound on `publishedAt`, ISO 8601. Unpublished posts are excluded when set.",
            "schema": { "type": "string" }, "example": "2026-01-01"
          },
          {
            "name": "publishedTo", "in": "query", "required": false,
            "description": "Inclusive upper bound on `publishedAt`, ISO 8601.",
            "schema": { "type": "string" }, "example": "2026-12-31T23:59:59Z"
          },
          {
            "name": "q", "in": "query", "required": false,
            "description": "Case-insensitive substring match on title, excerpt, and keywords. Article bodies are not searched.",
            "schema": { "type": "string" }
          },
          { "name": "featured", "in": "query", "required": false, "schema": { "type": "boolean" } }
        ],
        "responses": {
          "200": {
            "description": "A page of posts",
            "headers": {
              "ETag": { "$ref": "#/components/headers/ETag" },
              "Cache-Control": { "$ref": "#/components/headers/CacheControl" },
              "X-RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PostList" } } }
          },
          "304": { "$ref": "#/components/responses/NotModified" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/posts/{idOrSlug}": {
      "get": {
        "tags": ["Posts"],
        "operationId": "getPost",
        "summary": "Get one post",
        "description": "Accepts the post id or its slug. Returns every field, including `contentHtml`.",
        "parameters": [
          { "name": "idOrSlug", "in": "path", "required": true, "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/fields" }
        ],
        "responses": {
          "200": {
            "description": "The post",
            "headers": {
              "ETag": { "$ref": "#/components/headers/ETag" },
              "Cache-Control": { "$ref": "#/components/headers/CacheControl" }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Post" } } }
          },
          "304": { "$ref": "#/components/responses/NotModified" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/categories": {
      "get": {
        "tags": ["Taxonomy"],
        "operationId": "listCategories",
        "summary": "List categories",
        "parameters": [
          { "$ref": "#/components/parameters/limit" },
          { "$ref": "#/components/parameters/cursor" },
          { "name": "sort", "in": "query", "required": false, "schema": { "type": "string", "enum": ["name", "slug"], "default": "name" } },
          { "$ref": "#/components/parameters/order" },
          { "$ref": "#/components/parameters/fields" }
        ],
        "responses": {
          "200": {
            "description": "A page of categories",
            "headers": { "ETag": { "$ref": "#/components/headers/ETag" } },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TaxonomyList" } } }
          },
          "304": { "$ref": "#/components/responses/NotModified" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/categories/{idOrSlug}": {
      "get": {
        "tags": ["Taxonomy"],
        "operationId": "getCategory",
        "summary": "Get one category",
        "parameters": [
          { "name": "idOrSlug", "in": "path", "required": true, "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/fields" }
        ],
        "responses": {
          "200": {
            "description": "The category",
            "headers": { "ETag": { "$ref": "#/components/headers/ETag" } },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Taxonomy" } } }
          },
          "304": { "$ref": "#/components/responses/NotModified" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/tags": {
      "get": {
        "tags": ["Taxonomy"],
        "operationId": "listTags",
        "summary": "List tags",
        "parameters": [
          { "$ref": "#/components/parameters/limit" },
          { "$ref": "#/components/parameters/cursor" },
          { "name": "sort", "in": "query", "required": false, "schema": { "type": "string", "enum": ["name", "slug"], "default": "name" } },
          { "$ref": "#/components/parameters/order" },
          { "$ref": "#/components/parameters/fields" }
        ],
        "responses": {
          "200": {
            "description": "A page of tags",
            "headers": { "ETag": { "$ref": "#/components/headers/ETag" } },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TaxonomyList" } } }
          },
          "304": { "$ref": "#/components/responses/NotModified" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/tags/{idOrSlug}": {
      "get": {
        "tags": ["Taxonomy"],
        "operationId": "getTag",
        "summary": "Get one tag",
        "parameters": [
          { "name": "idOrSlug", "in": "path", "required": true, "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/fields" }
        ],
        "responses": {
          "200": {
            "description": "The tag",
            "headers": { "ETag": { "$ref": "#/components/headers/ETag" } },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Taxonomy" } } }
          },
          "304": { "$ref": "#/components/responses/NotModified" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/planned-topics": {
      "get": {
        "tags": ["Planning"],
        "operationId": "listPlannedTopics",
        "summary": "List planned topics",
        "description": "The content calendar: ideas queued for generation, and what became of them.",
        "parameters": [
          { "$ref": "#/components/parameters/limit" },
          { "$ref": "#/components/parameters/cursor" },
          { "name": "sort", "in": "query", "required": false, "schema": { "type": "string", "enum": ["createdAt", "scheduledFor", "title"], "default": "createdAt" } },
          { "$ref": "#/components/parameters/order" },
          { "$ref": "#/components/parameters/fields" },
          { "name": "status", "in": "query", "required": false, "schema": { "type": "string", "enum": ["proposed", "approved", "generated", "skipped"] } },
          { "name": "source", "in": "query", "required": false, "schema": { "type": "string", "enum": ["user", "ai", "research"] } },
          { "name": "scheduledFrom", "in": "query", "required": false, "description": "Inclusive lower bound on `scheduledFor`, ISO 8601.", "schema": { "type": "string" } },
          { "name": "scheduledTo", "in": "query", "required": false, "description": "Inclusive upper bound on `scheduledFor`, ISO 8601.", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "A page of planned topics",
            "headers": { "ETag": { "$ref": "#/components/headers/ETag" } },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlannedTopicList" } } }
          },
          "304": { "$ref": "#/components/responses/NotModified" },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/planned-topics/{id}": {
      "get": {
        "tags": ["Planning"],
        "operationId": "getPlannedTopic",
        "summary": "Get one planned topic",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
          { "$ref": "#/components/parameters/fields" }
        ],
        "responses": {
          "200": {
            "description": "The planned topic",
            "headers": { "ETag": { "$ref": "#/components/headers/ETag" } },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PlannedTopic" } } }
          },
          "304": { "$ref": "#/components/responses/NotModified" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "A site API key, e.g. `cb_live_a1b2c3d4e5f6.RANDOM_SECRET`. Create one in the dashboard under your site. The secret is shown once."
      }
    },
    "headers": {
      "ETag": {
        "description": "Validator for this exact response body. Send it back as `If-None-Match` to get a 304 instead of the payload.",
        "schema": { "type": "string" }
      },
      "CacheControl": {
        "description": "Always `private` — responses are scoped to one key and must never enter a shared cache.",
        "schema": { "type": "string" }
      },
      "RateLimitLimit": { "description": "Requests allowed per minute on this key.", "schema": { "type": "integer" } },
      "RateLimitRemaining": { "description": "Requests left in the current minute.", "schema": { "type": "integer" } },
      "RateLimitReset": { "description": "Unix seconds at which the window resets.", "schema": { "type": "integer" } },
      "RetryAfter": { "description": "Seconds to wait before retrying.", "schema": { "type": "integer" } },
      "RequestId": { "description": "Correlation id, echoed from your `X-Request-Id` when you send one.", "schema": { "type": "string" } }
    },
    "parameters": {
      "limit": {
        "name": "limit", "in": "query", "required": false,
        "description": "Rows per page, 1 to 100.",
        "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 25 }
      },
      "cursor": {
        "name": "cursor", "in": "query", "required": false,
        "description": "The `nextCursor` from the previous page, verbatim. Keep `sort` and `order` identical while paging.",
        "schema": { "type": "string" }
      },
      "order": {
        "name": "order", "in": "query", "required": false,
        "description": "Sort direction. Defaults to `desc` for date sorts and `asc` for name sorts.",
        "schema": { "type": "string", "enum": ["asc", "desc"] }
      },
      "fields": {
        "name": "fields", "in": "query", "required": false,
        "description": "Comma-separated subset of fields to return. `id` is always included.",
        "schema": { "type": "string" }
      }
    },
    "responses": {
      "NotModified": {
        "description": "Your `If-None-Match` matched. No body.",
        "headers": { "ETag": { "$ref": "#/components/headers/ETag" } }
      },
      "BadRequest": {
        "description": "A parameter was missing, malformed, or out of range.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Unauthorized": {
        "description": "No key, or a key that is unknown, revoked, or expired.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Forbidden": {
        "description": "The key lacks the required scope, or its site is deactivated.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "NotFound": {
        "description": "No resource on this site matches.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "RateLimited": {
        "description": "Too many requests in the current minute.",
        "headers": {
          "Retry-After": { "$ref": "#/components/headers/RetryAfter" },
          "X-RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
          "X-RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
        },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message", "requestId"],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable, machine-readable. Branch on this, never on `message`.",
                "enum": [
                  "missing_api_key", "invalid_api_key", "revoked_api_key", "expired_api_key",
                  "site_inactive", "insufficient_scope", "rate_limited",
                  "invalid_request", "invalid_cursor", "not_found", "conflict", "payload_too_large"
                ]
              },
              "message": { "type": "string", "description": "Written for a human reading a terminal." },
              "requestId": { "type": "string", "description": "Quote this to support." }
            }
          }
        },
        "example": {
          "error": {
            "code": "invalid_cursor",
            "message": "That cursor isn't valid. Use the nextCursor value from the previous response verbatim, or omit it to start from the first page.",
            "requestId": "0HN7GK2R1M9QK:00000003"
          }
        }
      },
      "Pagination": {
        "type": "object",
        "required": ["data", "hasMore", "nextCursor"],
        "properties": {
          "hasMore": { "type": "boolean" },
          "nextCursor": { "type": ["string", "null"], "description": "Null on the last page. That is the termination condition." }
        }
      },
      "TaxonomyRef": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "slug": { "type": "string" },
          "name": { "type": "string" }
        }
      },
      "Source": {
        "type": "object",
        "description": "A citation recorded while the post was written.",
        "properties": {
          "title": { "type": ["string", "null"] },
          "url": { "type": ["string", "null"] }
        }
      },
      "Post": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "slug": { "type": "string" },
          "title": { "type": "string" },
          "excerpt": { "type": ["string", "null"] },
          "status": { "type": "string", "enum": ["draft", "published"] },
          "publishedAt": { "type": ["string", "null"], "description": "ISO 8601 UTC, e.g. 2026-07-27T09:31:00Z. Null for drafts." },
          "createdAt": { "type": "string", "description": "ISO 8601 UTC." },
          "readingMinutes": { "type": "integer" },
          "url": { "type": "string", "description": "Where this post is publicly served — your own domain when you have configured one." },
          "coverImageUrl": { "type": ["string", "null"] },
          "ogImageUrl": { "type": ["string", "null"] },
          "imageAttribution": { "type": ["string", "null"] },
          "imageSourceUrl": { "type": ["string", "null"] },
          "category": { "oneOf": [{ "$ref": "#/components/schemas/TaxonomyRef" }, { "type": "null" }] },
          "tags": { "type": "array", "items": { "$ref": "#/components/schemas/TaxonomyRef" } },
          "keywords": { "type": ["string", "null"] },
          "metaTitle": { "type": ["string", "null"] },
          "metaDescription": { "type": ["string", "null"] },
          "viewCount": { "type": "integer" },
          "featured": { "type": "boolean" },
          "sources": { "type": "array", "items": { "$ref": "#/components/schemas/Source" }, "description": "Omitted from list responses unless requested with `fields`." },
          "contentHtml": { "type": "string", "description": "The article body. Omitted from list responses unless requested with `fields`." }
        }
      },
      "PostList": {
        "allOf": [
          { "$ref": "#/components/schemas/Pagination" },
          { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Post" } } } }
        ]
      },
      "Taxonomy": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "slug": { "type": "string" },
          "name": { "type": "string" },
          "publishedPostCount": { "type": "integer" }
        }
      },
      "TaxonomyList": {
        "allOf": [
          { "$ref": "#/components/schemas/Pagination" },
          { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Taxonomy" } } } }
        ]
      },
      "PlannedTopic": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "title": { "type": "string" },
          "contextNote": { "type": ["string", "null"] },
          "status": { "type": "string", "enum": ["proposed", "approved", "generated", "skipped"] },
          "source": { "type": "string", "enum": ["user", "ai", "research"] },
          "scheduledFor": { "type": ["string", "null"], "description": "ISO 8601 UTC." },
          "createdAt": { "type": "string", "description": "ISO 8601 UTC." },
          "generatedAt": { "type": ["string", "null"], "description": "ISO 8601 UTC." },
          "postId": { "type": ["string", "null"], "format": "uuid", "description": "The post this topic became, once generated." }
        }
      },
      "PlannedTopicList": {
        "allOf": [
          { "$ref": "#/components/schemas/Pagination" },
          { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/PlannedTopic" } } } }
        ]
      },
      "Site": {
        "type": "object",
        "properties": {
          "slug": { "type": "string" },
          "name": { "type": "string" },
          "tagline": { "type": ["string", "null"] },
          "description": { "type": ["string", "null"] },
          "url": { "type": "string", "description": "Where the blog is publicly served." },
          "websiteUrl": { "type": ["string", "null"], "description": "Your own website, the one the blog links back to." },
          "locale": { "type": "string" },
          "niche": { "type": "string" },
          "locations": { "type": "array", "items": { "type": "string" } },
          "author": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "name": { "type": ["string", "null"] },
                  "bio": { "type": ["string", "null"] },
                  "url": { "type": ["string", "null"] }
                }
              },
              { "type": "null" }
            ]
          },
          "aiDisclosure": { "type": "boolean" },
          "listedOnCrediblog": { "type": "boolean" },
          "publishedPostCount": { "type": "integer" },
          "updatedAt": { "type": ["string", "null"], "description": "ISO 8601 UTC." }
        }
      },
      "Index": {
        "type": "object",
        "properties": {
          "version": { "type": "string" },
          "site": { "type": "string" },
          "resources": { "type": "object", "additionalProperties": { "type": "string" } },
          "openapi": { "type": "string" },
          "documentation": { "type": "string" },
          "key": {
            "type": "object",
            "properties": {
              "prefix": { "type": "string" },
              "name": { "type": "string" },
              "scopes": { "type": "array", "items": { "type": "string", "enum": ["read", "write"] } }
            }
          },
          "rateLimit": {
            "type": "object",
            "properties": {
              "plan": { "type": "string" },
              "requestsPerMinute": { "type": "integer" }
            }
          }
        }
      }
    }
  }
}
