{
  "openapi": "3.1.0",
  "info": {
    "title": "InsuraCentral Public API",
    "version": "1.1.0",
    "summary": "Public, unauthenticated read API for InsuraCentral content.",
    "description": "InsuraCentral is a CRM and multi-line dialer built for life insurance, final expense, mortgage protection and IUL producers.\n\nThis specification documents only the **public, unauthenticated** surface. The authenticated platform API (leads, dialing, policies, agency management) is not described here and requires a per-account API key issued by InsuraCentral; contact support@insuracentral.com for partner access.\n\nAll responses are wrapped in a standard envelope: `{ statusCode, message, data, timestamp, timeTakenMs, path }`. The useful payload is at `data.data`.\n\n**Errors** are always JSON (never HTML) and follow the `Error` schema: a numeric `statusCode`, a machine-readable `message` code, a human-readable `error`, and where useful a `resolution` hint.\n\n**Scopes**: see the top-level `x-scopes` object. Public read operations require no credentials.\n\n**Versioning**: the major version is the `/v1/` path segment. Breaking changes ship as `/v2` and `/v1` keeps working. Deprecated operations return RFC 8594 `Deprecation` and `Sunset` headers and are marked `deprecated: true` in this document, with at least 6 months between announcement and removal.\n\n**Rate limits**: responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`; a 429 also carries `Retry-After`. Public reads allow 600 requests/minute per IP.",
    "contact": {
      "name": "InsuraCentral Support",
      "email": "support@insuracentral.com",
      "url": "https://insuracentral.com"
    },
    "termsOfService": "https://insuracentral.com/terms",
    "license": {
      "name": "Proprietary",
      "url": "https://insuracentral.com/terms"
    },
    "x-api-versioning": {
      "strategy": "URI path versioning — the major version is the `/v1/` segment.",
      "current": "v1",
      "supported": [
        "v1"
      ],
      "breakingChangePolicy": "Breaking changes ship under a new path segment (/v2). v1 keeps working.",
      "deprecationPolicy": "A deprecated operation returns the RFC 8594 `Deprecation` header and a `Sunset` header with the removal date, and is marked `deprecated: true` here. Minimum 6 months between Sunset announcement and removal.",
      "changelog": "https://docs.insuracentral.com"
    },
    "x-protected-resource-metadata": "https://insuracentral.com/.well-known/oauth-protected-resource"
  },
  "servers": [
    {
      "url": "https://app.insuracentral.com/api",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Blog",
      "description": "Published editorial content. Drafts are never returned."
    }
  ],
  "paths": {
    "/v1/public/blog/posts": {
      "get": {
        "operationId": "listBlogPosts",
        "tags": [
          "Blog"
        ],
        "summary": "List published blog posts",
        "description": "Returns published posts, newest first. Posts in draft status are excluded. Rate limited to 600 requests per minute per IP.",
        "parameters": [
          {
            "name": "per_page",
            "in": "query",
            "required": false,
            "description": "Items per page (default 15, maximum 100).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 15
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-indexed page number.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of published posts.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "success": {
                              "type": "boolean"
                            },
                            "data": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/BlogPost"
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the current window."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until the window resets."
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "default": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {}
        ],
        "x-scope": "content:read"
      }
    },
    "/v1/public/blog/posts/{slug}": {
      "get": {
        "operationId": "getBlogPost",
        "tags": [
          "Blog"
        ],
        "summary": "Get a single published post by slug",
        "description": "Returns one published post. Returns 404 if the slug does not exist or the post is a draft.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "URL slug, e.g. `life-insurance-crm`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The post.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "success": {
                              "type": "boolean"
                            },
                            "data": {
                              "$ref": "#/components/schemas/BlogPost"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests permitted in the current window."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current window."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until the window resets."
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "default": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {}
        ],
        "x-scope": "content:read"
      }
    }
  },
  "components": {
    "schemas": {
      "Envelope": {
        "type": "object",
        "description": "Standard response wrapper used by every endpoint.",
        "properties": {
          "statusCode": {
            "type": "integer",
            "examples": [
              200
            ]
          },
          "message": {
            "type": "string",
            "examples": [
              "Request successful"
            ]
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "timeTakenMs": {
            "type": "integer"
          },
          "path": {
            "type": "string"
          }
        }
      },
      "BlogPost": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "slug": {
            "type": "string",
            "description": "URL segment under https://insuracentral.com/blog/"
          },
          "short_description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Meta description / summary."
          },
          "content": {
            "type": "string",
            "description": "Sanitized HTML body."
          },
          "status": {
            "type": "string",
            "enum": [
              "published"
            ],
            "description": "Only published posts are returned publicly."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "title",
          "slug",
          "content",
          "status"
        ]
      },
      "Error": {
        "type": "object",
        "description": "Every error is JSON with this shape. No HTML error pages are ever returned by the API.",
        "properties": {
          "statusCode": {
            "type": "integer",
            "description": "HTTP status, repeated in the body.",
            "examples": [
              404
            ]
          },
          "message": {
            "type": "string",
            "description": "Machine-readable code.",
            "examples": [
              "NOT_FOUND"
            ]
          },
          "error": {
            "type": "string",
            "description": "Human-readable explanation.",
            "examples": [
              "Not found"
            ]
          },
          "resolution": {
            "type": "string",
            "description": "Hint for how an agent should recover.",
            "examples": [
              "Check the slug against GET /v1/public/blog/posts, or fetch https://insuracentral.com/sitemap.xml"
            ]
          },
          "path": {
            "type": "string"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "null"
          }
        },
        "required": [
          "statusCode",
          "message"
        ]
      },
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem detail. Returned when the client sends `Accept: application/problem+json`.",
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "examples": [
              "https://insuracentral.com/errors/not-found"
            ]
          },
          "title": {
            "type": "string",
            "examples": [
              "Not Found"
            ]
          },
          "status": {
            "type": "integer",
            "examples": [
              404
            ]
          },
          "detail": {
            "type": "string"
          },
          "instance": {
            "type": "string"
          }
        },
        "required": [
          "type",
          "title",
          "status"
        ]
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid parameters. `message` names the offending field.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "NotFound": {
        "description": "No published resource matches the identifier.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests. Retry after the interval in `Retry-After`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "ServerError": {
        "description": "Unexpected server error. Safe to retry with backoff.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      }
    },
    "securitySchemes": {
      "PublicRead": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Not required for the public read endpoints in this document. Partner keys are issued per account by InsuraCentral and are scoped to a role; see `x-scopes` for the roles a key can hold. Request access at support@insuracentral.com."
      },
      "PartnerApiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Per-account partner key. Every key carries exactly one role, and a role grants only the operations listed for it in `x-scopes`. Keys are hashed at rest and can be revoked without affecting other integrations.",
        "x-protected-resource-metadata": "https://insuracentral.com/.well-known/oauth-protected-resource"
      }
    }
  },
  "externalDocs": {
    "description": "Plain-language summary of the platform for AI agents",
    "url": "https://insuracentral.com/llms.txt"
  },
  "x-scopes": {
    "content:read": "Read published blog content. No authentication required; this is the surface documented here.",
    "content:write": "Create and update editorial content. Content created with this scope is always created as a DRAFT and requires human approval before publication.",
    "content:publish": "Publish or unpublish content. Held only by editorial-desk keys, never by machine integrations.",
    "leads:write": "Submit leads into a producer's account. Partner scope, issued per integration."
  },
  "security": [
    {}
  ]
}