{
  "openapi": "3.1.0",
  "info": {
    "title": "Necto Consulting API",
    "version": "1.1.0",
    "description": "Reading is public. Writing needs a Bearer token issued by a person (https://necto-consulting.pages.dev/.well-known/auth.md). Docs: https://necto-consulting.pages.dev/developers/. Every /api/* error is one JSON shape (Error): status, code, message, hint, docs, and errors[] when there are several. Limited endpoints send RateLimit, RateLimit-Policy and X-RateLimit-* on every answer, and Retry-After on a 429. Versions: send X-Content-API-Version: 1 to lock the major version; another answers 400 unsupported_version. A breaking change gets a new major. An endpoint to be removed carries Deprecation and Sunset first, for at least 90 days. Deprecations: none announced."
  },
  "x-api-lifecycle": {
    "versionHeader": "X-Content-API-Version",
    "major": 1,
    "sunsetMinDays": 90,
    "deprecations": []
  },
  "servers": [
    {
      "url": "https://necto-consulting.pages.dev"
    }
  ],
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "A token issued by a person: see auth.md"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "status",
          "code",
          "message",
          "hint",
          "docs"
        ],
        "properties": {
          "status": {
            "const": "error"
          },
          "code": {
            "type": "string",
            "enum": [
              "bad_request",
              "invalid_json",
              "validation_failed",
              "unsupported_version",
              "unauthorized",
              "forbidden",
              "not_found",
              "method_not_allowed",
              "conflict",
              "idempotency_in_progress",
              "payload_too_large",
              "idempotency_key_reused",
              "rate_limited",
              "internal_error",
              "bad_gateway",
              "unavailable"
            ]
          },
          "message": {
            "type": "string"
          },
          "hint": {
            "type": "string"
          },
          "docs": {
            "type": "string",
            "format": "uri"
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            },
            "description": "One item per problem, e.g. { field, message }"
          },
          "version": {
            "type": "string"
          },
          "error": {
            "type": "string",
            "description": "The one-line error of the Visibilio Connect contract, kept beside message"
          }
        }
      },
      "ApiIndex": {
        "type": "object",
        "required": [
          "status",
          "version",
          "docs",
          "endpoints"
        ],
        "properties": {
          "status": {
            "const": "ok"
          },
          "version": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "docs": {
            "type": "string",
            "format": "uri"
          },
          "openapi": {
            "type": [
              "string",
              "null"
            ]
          },
          "auth": {
            "type": "string",
            "format": "uri"
          },
          "endpoints": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "method",
                "path",
                "auth",
                "description"
              ],
              "properties": {
                "method": {
                  "type": "string"
                },
                "path": {
                  "type": "string"
                },
                "auth": {
                  "type": "string"
                },
                "description": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Health": {
        "type": "object",
        "required": [
          "status",
          "time"
        ],
        "properties": {
          "status": {
            "const": "ok"
          },
          "time": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Version": {
        "type": "object",
        "required": [
          "status",
          "version",
          "render_capabilities"
        ],
        "properties": {
          "status": {
            "const": "ok"
          },
          "version": {
            "type": "string"
          },
          "render_capabilities": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "renders_faqs": {
                "type": "boolean"
              },
              "renders_summary": {
                "type": "boolean"
              },
              "renders_breadcrumbs": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "ArticleCreate": {
        "type": "object",
        "additionalProperties": true,
        "required": [
          "slug",
          "title",
          "body",
          "rubric"
        ],
        "description": "A new article, flat (the Visibilio nested payload is accepted too). tags, status (published) and lang (the default locale) have defaults.",
        "properties": {
          "slug": {
            "type": "string",
            "description": "Lowercase ASCII letters, digits and hyphens, at most 80 characters"
          },
          "title": {
            "type": "string"
          },
          "body": {
            "type": "string",
            "description": "HTML, sanitised on write"
          },
          "rubric": {
            "type": "string",
            "description": "A category title or slug from the catalog"
          },
          "lang": {
            "type": "string",
            "enum": [
              "en"
            ]
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "excerpt": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "published",
              "draft"
            ]
          },
          "published_at": {
            "type": "string",
            "format": "date-time"
          },
          "translation_of": {
            "type": "string",
            "description": "The slug of the published article in another language this one translates"
          },
          "author": {
            "type": "string"
          },
          "author_slug": {
            "type": "string"
          },
          "meta_title": {
            "type": "string",
            "maxLength": 41
          },
          "featured": {
            "type": "boolean"
          },
          "cover_image_url": {
            "type": "string",
            "description": "An absolute https URL or a path on this site; any other value shows no image"
          },
          "content": {
            "type": "object",
            "description": "The AEO blocks. A PUT changes them block by block: an empty list removes a block, one it does not carry stays",
            "properties": {
              "faq": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "q": {
                      "type": "string"
                    },
                    "a": {
                      "type": "string"
                    }
                  }
                }
              },
              "key_takeaways": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "sources": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "title"
                  ],
                  "properties": {
                    "title": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string",
                      "format": "uri",
                      "description": "An absolute http(s) URL; leave it out for a source without a link"
                    },
                    "publisher": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "services": {
            "type": "array",
            "maxItems": 5,
            "items": {
              "type": "string"
            },
            "description": "The services this article supports: a slug or title from the catalog. On a PUT the list replaces them and an empty list clears them"
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate only: nothing is written"
          }
        }
      },
      "ArticlePatch": {
        "type": "object",
        "additionalProperties": true,
        "description": "A partial update: only the fields it carries change, and none is required. null is the same as leaving a field out, except translation_of, where null (or an empty string) clears the pair; an empty services list or AEO block clears it. slug is ignored: the path names the article.",
        "properties": {
          "slug": {
            "type": [
              "string",
              "null"
            ],
            "description": "Ignored: the path names the article"
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "body": {
            "type": [
              "string",
              "null"
            ],
            "description": "HTML, sanitised on write"
          },
          "rubric": {
            "type": [
              "string",
              "null"
            ],
            "description": "A category title or slug from the catalog"
          },
          "lang": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "en",
              null
            ]
          },
          "tags": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            }
          },
          "excerpt": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "published",
              "draft",
              null
            ]
          },
          "published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "translation_of": {
            "type": [
              "string",
              "null"
            ],
            "description": "The slug of the published article in another language this one translates"
          },
          "author": {
            "type": [
              "string",
              "null"
            ]
          },
          "author_slug": {
            "type": [
              "string",
              "null"
            ]
          },
          "meta_title": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 41
          },
          "featured": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "cover_image_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "An absolute https URL or a path on this site; any other value shows no image"
          },
          "content": {
            "type": [
              "object",
              "null"
            ],
            "description": "The AEO blocks. A PUT changes them block by block: an empty list removes a block, one it does not carry stays",
            "properties": {
              "faq": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "q": {
                      "type": "string"
                    },
                    "a": {
                      "type": "string"
                    }
                  }
                }
              },
              "key_takeaways": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "sources": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "title"
                  ],
                  "properties": {
                    "title": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string",
                      "format": "uri",
                      "description": "An absolute http(s) URL; leave it out for a source without a link"
                    },
                    "publisher": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "services": {
            "type": [
              "array",
              "null"
            ],
            "maxItems": 5,
            "items": {
              "type": "string"
            },
            "description": "The services this article supports: a slug or title from the catalog. On a PUT the list replaces them and an empty list clears them"
          },
          "dry_run": {
            "type": "boolean",
            "description": "Validate only: nothing is written"
          }
        }
      },
      "Article": {
        "type": "object",
        "additionalProperties": true,
        "required": [
          "slug",
          "title",
          "rubric",
          "lang",
          "status",
          "body"
        ],
        "description": "An article as stored",
        "properties": {
          "slug": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "body": {
            "type": "string",
            "description": "HTML, sanitised on write"
          },
          "rubric": {
            "type": "string",
            "description": "The category slug"
          },
          "lang": {
            "type": "string",
            "enum": [
              "en"
            ]
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "excerpt": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "published",
              "draft"
            ]
          },
          "published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "translation_of": {
            "type": [
              "string",
              "null"
            ],
            "description": "The twin slug; null or an empty string clears the pair"
          },
          "author": {
            "type": "string"
          },
          "author_slug": {
            "type": "string"
          },
          "meta_title": {
            "type": "string"
          },
          "featured": {
            "type": "boolean"
          },
          "cover_image_url": {
            "type": "string",
            "description": "An absolute https URL or a path on this site; any other value shows no image"
          },
          "content": {
            "type": "object",
            "description": "The AEO blocks. A PUT changes them block by block: an empty list removes a block, one it does not carry stays",
            "properties": {
              "faq": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "q": {
                      "type": "string"
                    },
                    "a": {
                      "type": "string"
                    }
                  }
                }
              },
              "key_takeaways": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "sources": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "title"
                  ],
                  "properties": {
                    "title": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string",
                      "format": "uri",
                      "description": "An absolute http(s) URL; leave it out for a source without a link"
                    },
                    "publisher": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "services": {
            "type": "array",
            "maxItems": 5,
            "items": {
              "type": "string"
            },
            "description": "The services this article supports: a slug or title from the catalog. On a PUT the list replaces them and an empty list clears them"
          },
          "rubric_title": {
            "type": "string"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "topics": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Created": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "status",
              "version",
              "ok",
              "slug",
              "url"
            ],
            "properties": {
              "status": {
                "const": "ok"
              },
              "version": {
                "type": "string"
              },
              "ok": {
                "const": true
              },
              "slug": {
                "type": "string"
              },
              "url": {
                "type": "string"
              },
              "warnings": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/BodyWarning"
                },
                "description": "Advice, never an error: what the article misses of the AEO body contract (answer first, absolute dates, sourced claims, self-contained sections, three buyer questions)"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "status",
              "version",
              "dry_run",
              "persisted"
            ],
            "properties": {
              "status": {
                "const": "ok"
              },
              "version": {
                "type": "string"
              },
              "dry_run": {
                "const": true
              },
              "persisted": {
                "const": false
              },
              "slug": {
                "type": "string"
              },
              "url": {
                "type": "string"
              },
              "status_after": {
                "type": "string"
              },
              "categories": {
                "type": "array",
                "items": {
                  "type": "object"
                }
              },
              "services": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "slug",
                    "title"
                  ],
                  "properties": {
                    "slug": {
                      "type": "string"
                    },
                    "group": {
                      "type": "string"
                    },
                    "title": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "warnings": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/BodyWarning"
                },
                "description": "Advice, never an error: what the article misses of the AEO body contract (answer first, absolute dates, sourced claims, self-contained sections, three buyer questions)"
              }
            }
          }
        ]
      },
      "ArticleResponse": {
        "type": "object",
        "required": [
          "status",
          "version",
          "article"
        ],
        "properties": {
          "status": {
            "const": "ok"
          },
          "version": {
            "type": "string"
          },
          "article": {
            "$ref": "#/components/schemas/Article"
          }
        }
      },
      "Updated": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "status",
              "version",
              "ok",
              "slug",
              "url"
            ],
            "properties": {
              "status": {
                "const": "ok"
              },
              "version": {
                "type": "string"
              },
              "ok": {
                "const": true
              },
              "slug": {
                "type": "string"
              },
              "url": {
                "type": "string"
              },
              "warnings": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/BodyWarning"
                },
                "description": "Advice, never an error: what the article misses of the AEO body contract (answer first, absolute dates, sourced claims, self-contained sections, three buyer questions)"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "status",
              "version",
              "dry_run",
              "persisted",
              "slug",
              "url"
            ],
            "properties": {
              "status": {
                "const": "ok"
              },
              "version": {
                "type": "string"
              },
              "dry_run": {
                "const": true
              },
              "persisted": {
                "const": false
              },
              "slug": {
                "type": "string"
              },
              "url": {
                "type": "string"
              },
              "status_after": {
                "type": "string"
              },
              "warnings": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/BodyWarning"
                },
                "description": "Advice, never an error: what the article misses of the AEO body contract (answer first, absolute dates, sourced claims, self-contained sections, three buyer questions)"
              }
            }
          }
        ]
      },
      "Deleted": {
        "type": "object",
        "required": [
          "status",
          "version",
          "ok",
          "deleted"
        ],
        "properties": {
          "status": {
            "const": "ok"
          },
          "version": {
            "type": "string"
          },
          "ok": {
            "const": true
          },
          "deleted": {
            "type": "string"
          }
        }
      },
      "Catalog": {
        "type": "object",
        "required": [
          "status",
          "version",
          "categories",
          "tags",
          "articles"
        ],
        "properties": {
          "status": {
            "const": "ok"
          },
          "version": {
            "type": "string"
          },
          "count": {
            "type": "integer",
            "description": "Every published article, on every page"
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "articles": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "GET: one page, newest first; POST: every article"
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "GET: send it back as ?cursor= for the next page; null on the last page"
          },
          "services": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "slug",
                "title"
              ],
              "properties": {
                "slug": {
                  "type": "string"
                },
                "group": {
                  "type": "string"
                },
                "title": {
                  "type": "object",
                  "additionalProperties": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      },
      "JsonRpc": {
        "type": "object",
        "required": [
          "jsonrpc"
        ],
        "properties": {
          "jsonrpc": {
            "const": "2.0"
          },
          "id": {},
          "method": {
            "type": "string"
          },
          "params": {
            "type": "object"
          },
          "result": {},
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "integer"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "RateLimited": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "const": "rate_limited"
          },
          "error_description": {
            "type": "string"
          }
        }
      },
      "BodyWarning": {
        "type": "object",
        "required": [
          "rule",
          "message"
        ],
        "properties": {
          "rule": {
            "type": "string",
            "enum": [
              "relative-date",
              "superlative",
              "answer-first",
              "back-reference",
              "questions",
              "unchecked"
            ]
          },
          "message": {
            "type": "string"
          },
          "phrase": {
            "type": "string"
          },
          "where": {
            "type": "string",
            "enum": [
              "body",
              "faq",
              "key_takeaways"
            ]
          },
          "section": {
            "type": "string"
          },
          "excerpt": {
            "type": "string"
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "Tags": {
        "type": "object",
        "required": [
          "status",
          "version",
          "tags"
        ],
        "properties": {
          "status": {
            "const": "ok"
          },
          "version": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "ImageUpload": {
        "type": "object",
        "required": [
          "file",
          "slug"
        ],
        "properties": {
          "file": {
            "type": "string",
            "format": "binary",
            "description": "WebP, JPEG, PNG, GIF or AVIF, read from its bytes"
          },
          "slug": {
            "type": "string",
            "description": "The article the image belongs to; it need not exist yet"
          },
          "variant": {
            "type": "string",
            "description": "featured (the default), og, square or thumbnail keeps a fixed path; any other name is stored under its content hash"
          },
          "dry_run": {
            "type": "string",
            "enum": [
              "1",
              "true"
            ],
            "description": "Check only: nothing is written"
          }
        }
      },
      "ImageStored": {
        "type": "object",
        "required": [
          "status",
          "version",
          "ok",
          "image"
        ],
        "properties": {
          "status": {
            "const": "ok"
          },
          "version": {
            "type": "string"
          },
          "ok": {
            "const": true
          },
          "dry_run": {
            "const": true
          },
          "persisted": {
            "const": false
          },
          "image": {
            "type": "object",
            "required": [
              "key",
              "url",
              "type",
              "size"
            ],
            "properties": {
              "key": {
                "type": "string"
              },
              "url": {
                "type": "string",
                "description": "Relative to the site"
              },
              "type": {
                "type": "string"
              },
              "size": {
                "type": "integer"
              }
            }
          }
        }
      }
    },
    "parameters": {
      "Slug": {
        "name": "slug",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "maxLength": 255
        },
        "description": "Retry safely: the same key and body return the first answer for 24 hours; another body with the key answers 422."
      },
      "ContentApiVersion": {
        "name": "X-Content-API-Version",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string"
        },
        "description": "The major version the client was written for (1); another one answers 400 unsupported_version."
      },
      "DryRun": {
        "name": "dry_run",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "1",
            "true"
          ]
        },
        "description": "Validate only: the answer says what would be created or what the record would become, and nothing is written."
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string"
        },
        "description": "The next_cursor of the page before, as it came (opaque); leave it out for the first page. One not in the shape this API issues answers 400."
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 500,
          "default": 100
        },
        "description": "Articles a page, at most: 100 unless sent; more than 500 is read as 500."
      },
      "McpProtocolVersion": {
        "name": "MCP-Protocol-Version",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string"
        },
        "description": "MCP 2026-07-28: required, the same as params._meta[\"io.modelcontextprotocol/protocolVersion\"] (else 400, JSON-RPC -32020); a version this server does not speak answers 400, -32022, with data.supported (2026-07-28, 2025-06-18). MCP 2025-06-18 (initialize first): optional."
      },
      "McpMethod": {
        "name": "Mcp-Method",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string"
        },
        "description": "MCP 2026-07-28: required, the same as the body's method (else 400, -32020)."
      },
      "McpName": {
        "name": "Mcp-Name",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string"
        },
        "description": "MCP 2026-07-28, tools/call: required, the same as params.name, plain or as =?base64?{the UTF-8 name in Base64}?= (else 400, -32020)."
      }
    },
    "headers": {
      "RateLimit": {
        "description": "Where this client stands: \"<policy>\";r=<remaining>;t=<seconds> (draft-ietf-httpapi-ratelimit-headers)",
        "schema": {
          "type": "string"
        }
      },
      "RateLimit-Policy": {
        "description": "The policy: \"<policy>\";q=<quota>;w=<window seconds>",
        "schema": {
          "type": "string"
        }
      },
      "X-RateLimit-Limit": {
        "description": "Requests allowed in the window",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Remaining": {
        "description": "Requests left in the window",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Reset": {
        "description": "When the window resets, Unix seconds",
        "schema": {
          "type": "integer"
        }
      },
      "Retry-After": {
        "description": "Seconds until a request can succeed",
        "schema": {
          "type": "integer"
        }
      },
      "WWW-Authenticate": {
        "description": "The Bearer challenge (RFC 6750): the realm, the protected resource metadata where the OAuth tier serves it (RFC 9728), error=\"invalid_token\" when a credential was sent, error=\"insufficient_scope\" with the scope a token lacks",
        "schema": {
          "type": "string"
        },
        "example": "Bearer realm=\"necto-consulting.com\", error=\"invalid_token\""
      },
      "WWW-Authenticate-Basic": {
        "description": "The Basic challenge (RFC 6749 section 5.2, RFC 9110): a refused client, with the realm",
        "schema": {
          "type": "string"
        },
        "example": "Basic realm=\"necto-consulting.com\""
      },
      "Idempotent-Replayed": {
        "description": "true: this is the stored answer to an earlier request with the same Idempotency-Key",
        "schema": {
          "type": "string",
          "enum": [
            "true"
          ]
        }
      },
      "X-Content-API-Version": {
        "description": "The version of the Content API that answered",
        "schema": {
          "type": "string"
        }
      },
      "Deprecation": {
        "description": "The endpoint is deprecated since this moment (RFC 9745)",
        "schema": {
          "type": "string"
        }
      },
      "Sunset": {
        "description": "The endpoint is removed after this date (RFC 8594)",
        "schema": {
          "type": "string"
        }
      },
      "Link-Next": {
        "description": "The next page (RFC 8288), while next_cursor is not null: <{origin}/api/content/catalog?cursor={next_cursor, URL-encoded}&limit={the request's own limit, when it sent one}>; rel=\"next\". Absent on the last page.",
        "schema": {
          "type": "string"
        },
        "example": "<https://necto-consulting.pages.dev/api/content/catalog?cursor=WyIyMDI2LTEwLTAxVDA4OjAwOjAwLjAwMFoiLCJjdXN0b21zLWhvbGQiXQ&limit=50>; rel=\"next\""
      }
    }
  },
  "paths": {
    "/api/": {
      "get": {
        "operationId": "listEndpoints",
        "summary": "This list",
        "description": "This list: every endpoint with its method, path and authentication, and where the docs are.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContentApiVersion"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-Content-API-Version": {
                "$ref": "#/components/headers/X-Content-API-Version"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiIndex"
                }
              }
            }
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "operationId": "health",
        "summary": "Liveness for agents and uptime monitors",
        "description": "Liveness for agents and uptime monitors. No dependency is probed.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContentApiVersion"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-Content-API-Version": {
                "$ref": "#/components/headers/X-Content-API-Version"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          }
        }
      }
    },
    "/api/content/version": {
      "get": {
        "operationId": "contentVersion",
        "summary": "The API version and what the article template renders (the VISIBILIO handshake)",
        "description": "The API version and what the article template renders (the VISIBILIO handshake).",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContentApiVersion"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-Content-API-Version": {
                "$ref": "#/components/headers/X-Content-API-Version"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Version"
                }
              }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "code": "unsupported_version",
                  "message": "Unsupported X-Content-API-Version: version 2 is not served: this API is version 1.",
                  "hint": "Send X-Content-API-Version: 1, or leave the header out.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-unsupported_version"
                }
              }
            }
          }
        }
      }
    },
    "/api/content/": {
      "post": {
        "operationId": "createArticle",
        "summary": "Create an article",
        "description": "Create an article. With dry_run it is validated and nothing is written; with Idempotency-Key a retry returns the first answer.",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContentApiVersion"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/DryRun"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ArticleCreate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Content-API-Version": {
                "$ref": "#/components/headers/X-Content-API-Version"
              },
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/Idempotent-Replayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Created"
                }
              }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "version": "1.1.0",
                  "error": "Validation failed",
                  "details": [
                    {
                      "field": "title",
                      "message": "title is required"
                    }
                  ],
                  "code": "validation_failed",
                  "message": "Validation failed",
                  "hint": "errors[] names each field and what it needs; fix them all and send again.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-validation_failed",
                  "errors": [
                    {
                      "field": "title",
                      "message": "title is required"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "No valid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "version": "1.1.0",
                  "error": "Unauthorized",
                  "code": "unauthorized",
                  "message": "Unauthorized",
                  "hint": "Send Authorization: Bearer <token>. Tokens are issued by a person: see /.well-known/auth.md.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-unauthorized"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "version": "1.1.0",
                  "error": "Already exists",
                  "details": [
                    {
                      "slug": "customs-hold"
                    }
                  ],
                  "code": "conflict",
                  "message": "Already exists",
                  "hint": "An article with this slug exists: update it with PUT /api/content/{slug}.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-conflict",
                  "errors": [
                    {
                      "slug": "customs-hold"
                    }
                  ]
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            }
          },
          "413": {
            "description": "Payload too large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "version": "1.1.0",
                  "error": "Payload too large",
                  "code": "payload_too_large",
                  "message": "Payload too large",
                  "hint": "An article with its images must stay under 12 MB.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-payload_too_large"
                }
              }
            }
          },
          "422": {
            "description": "Idempotency-Key reused with another body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "code": "idempotency_key_reused",
                  "message": "This Idempotency-Key was used with a different body.",
                  "hint": "Use a new key for a new request; a retry sends the same body with the same key.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-idempotency_key_reused"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "ok": false,
                  "status": "error",
                  "code": "rate_limited",
                  "message": "Too many requests.",
                  "hint": "Wait the Retry-After seconds; the RateLimit headers show the policy.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-rate_limited",
                  "error": "Too many requests"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            }
          },
          "503": {
            "description": "Unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "hint": "Nothing was stored: send the same request again after Retry-After seconds.",
                  "status": "error",
                  "version": "1.1.0",
                  "error": "The store is busy",
                  "details": [
                    {
                      "slug": "customs-hold",
                      "message": "KV refused the write (one write per key a second); nothing was stored"
                    }
                  ],
                  "code": "unavailable",
                  "message": "The store is busy",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-unavailable",
                  "errors": [
                    {
                      "slug": "customs-hold",
                      "message": "KV refused the write (one write per key a second); nothing was stored"
                    }
                  ]
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            }
          }
        }
      }
    },
    "/api/content/{slug}": {
      "get": {
        "operationId": "getArticle",
        "summary": "One published article",
        "description": "One published article. A draft answers 404.",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/Slug"
          },
          {
            "$ref": "#/components/parameters/ContentApiVersion"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-Content-API-Version": {
                "$ref": "#/components/headers/X-Content-API-Version"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ArticleResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "version": "1.1.0",
                  "error": "Not found",
                  "code": "not_found",
                  "message": "Not found",
                  "hint": "GET /api/ lists every endpoint; a draft article answers 404 in public.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-not_found"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "updateArticle",
        "summary": "Update an article",
        "description": "Update an article: only the fields the payload carries change. With dry_run it is validated and merged, and nothing is written.",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Slug"
          },
          {
            "$ref": "#/components/parameters/ContentApiVersion"
          },
          {
            "$ref": "#/components/parameters/DryRun"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ArticlePatch"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Content-API-Version": {
                "$ref": "#/components/headers/X-Content-API-Version"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Updated"
                }
              }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "version": "1.1.0",
                  "error": "Validation failed",
                  "details": [
                    {
                      "field": "title",
                      "message": "title is required"
                    }
                  ],
                  "code": "validation_failed",
                  "message": "Validation failed",
                  "hint": "errors[] names each field and what it needs; fix them all and send again.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-validation_failed",
                  "errors": [
                    {
                      "field": "title",
                      "message": "title is required"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "No valid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "version": "1.1.0",
                  "error": "Unauthorized",
                  "code": "unauthorized",
                  "message": "Unauthorized",
                  "hint": "Send Authorization: Bearer <token>. Tokens are issued by a person: see /.well-known/auth.md.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-unauthorized"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "version": "1.1.0",
                  "error": "Not found",
                  "code": "not_found",
                  "message": "Not found",
                  "hint": "GET /api/ lists every endpoint; a draft article answers 404 in public.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-not_found"
                }
              }
            }
          },
          "413": {
            "description": "Payload too large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "version": "1.1.0",
                  "error": "Payload too large",
                  "code": "payload_too_large",
                  "message": "Payload too large",
                  "hint": "An article with its images must stay under 12 MB.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-payload_too_large"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "ok": false,
                  "status": "error",
                  "code": "rate_limited",
                  "message": "Too many requests.",
                  "hint": "Wait the Retry-After seconds; the RateLimit headers show the policy.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-rate_limited",
                  "error": "Too many requests"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteArticle",
        "summary": "Remove an article and its images",
        "description": "Remove an article and its images.",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Slug"
          },
          {
            "$ref": "#/components/parameters/ContentApiVersion"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Content-API-Version": {
                "$ref": "#/components/headers/X-Content-API-Version"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Deleted"
                }
              }
            }
          },
          "401": {
            "description": "No valid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "version": "1.1.0",
                  "error": "Unauthorized",
                  "code": "unauthorized",
                  "message": "Unauthorized",
                  "hint": "Send Authorization: Bearer <token>. Tokens are issued by a person: see /.well-known/auth.md.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-unauthorized"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "version": "1.1.0",
                  "error": "Not found",
                  "code": "not_found",
                  "message": "Not found",
                  "hint": "GET /api/ lists every endpoint; a draft article answers 404 in public.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-not_found"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "ok": false,
                  "status": "error",
                  "code": "rate_limited",
                  "message": "Too many requests.",
                  "hint": "Wait the Retry-After seconds; the RateLimit headers show the policy.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-rate_limited",
                  "error": "Too many requests"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            }
          }
        }
      }
    },
    "/api/content/catalog": {
      "get": {
        "operationId": "contentCatalog",
        "summary": "Categories, tags and every published article (Visibilio Connect), the articles a page at a time",
        "description": "Categories, tags and every published article (Visibilio Connect), the articles a page at a time: follow next_cursor until it is null (the Link header names the same next page).",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContentApiVersion"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "$ref": "#/components/parameters/Limit"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-Content-API-Version": {
                "$ref": "#/components/headers/X-Content-API-Version"
              },
              "Link": {
                "$ref": "#/components/headers/Link-Next"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Catalog"
                }
              }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "version": "1.1.0",
                  "error": "Validation failed",
                  "details": [
                    {
                      "field": "cursor",
                      "message": "cursor is not in the shape this catalog issues: start again without it"
                    }
                  ],
                  "code": "validation_failed",
                  "message": "Validation failed",
                  "hint": "errors[] names each field and what it needs; fix them all and send again.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-validation_failed",
                  "errors": [
                    {
                      "field": "cursor",
                      "message": "cursor is not in the shape this catalog issues: start again without it"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "No valid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "version": "1.1.0",
                  "error": "Unauthorized",
                  "code": "unauthorized",
                  "message": "Unauthorized",
                  "hint": "Send Authorization: Bearer <token>. Tokens are issued by a person: see /.well-known/auth.md.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-unauthorized"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              }
            }
          }
        }
      },
      "post": {
        "operationId": "rebuildCatalog",
        "summary": "Rebuild the article lists and counts from the stored articles after parallel writes",
        "description": "Rebuild the article lists and counts from the stored articles after parallel writes: a dry run answering each change (changes[]) and the catalog it leaves; ?apply=1 writes them.",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContentApiVersion"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Content-API-Version": {
                "$ref": "#/components/headers/X-Content-API-Version"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Catalog"
                }
              }
            }
          },
          "401": {
            "description": "No valid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "version": "1.1.0",
                  "error": "Unauthorized",
                  "code": "unauthorized",
                  "message": "Unauthorized",
                  "hint": "Send Authorization: Bearer <token>. Tokens are issued by a person: see /.well-known/auth.md.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-unauthorized"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              }
            }
          },
          "429": {
            "description": "Too many requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "ok": false,
                  "status": "error",
                  "code": "rate_limited",
                  "message": "Too many requests.",
                  "hint": "Wait the Retry-After seconds; the RateLimit headers show the policy.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-rate_limited",
                  "error": "Too many requests"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            }
          }
        }
      }
    },
    "/api/content/tags": {
      "get": {
        "operationId": "contentTags",
        "summary": "The tags of the published articles, as the catalog lists them (Visibilio Connect)",
        "description": "The tags of the published articles, as the catalog lists them (Visibilio Connect).",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContentApiVersion"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-Content-API-Version": {
                "$ref": "#/components/headers/X-Content-API-Version"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tags"
                }
              }
            }
          },
          "401": {
            "description": "No valid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "version": "1.1.0",
                  "error": "Unauthorized",
                  "code": "unauthorized",
                  "message": "Unauthorized",
                  "hint": "Send Authorization: Bearer <token>. Tokens are issued by a person: see /.well-known/auth.md.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-unauthorized"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              }
            }
          }
        }
      }
    },
    "/api/content/images": {
      "post": {
        "operationId": "uploadImage",
        "summary": "Upload one image (multipart/form-data",
        "description": "Upload one image (multipart/form-data: file, slug, variant; at most 10 MB) into the site's own storage; the answer names its URL. With dry_run it is checked and nothing is written.",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ContentApiVersion"
          },
          {
            "$ref": "#/components/parameters/DryRun"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/ImageUpload"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Content-API-Version": {
                "$ref": "#/components/headers/X-Content-API-Version"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageStored"
                }
              }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "version": "1.1.0",
                  "error": "Validation failed",
                  "details": [
                    {
                      "field": "file",
                      "message": "a file part named file is required"
                    }
                  ],
                  "code": "validation_failed",
                  "message": "Validation failed",
                  "hint": "errors[] names each field and what it needs; fix them all and send again.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-validation_failed",
                  "errors": [
                    {
                      "field": "file",
                      "message": "a file part named file is required"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "No valid credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "status": "error",
                  "version": "1.1.0",
                  "error": "Unauthorized",
                  "code": "unauthorized",
                  "message": "Unauthorized",
                  "hint": "Send Authorization: Bearer <token>. Tokens are issued by a person: see /.well-known/auth.md.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-unauthorized"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              }
            }
          },
          "413": {
            "description": "Payload too large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "hint": "An image must stay under 10 MB.",
                  "status": "error",
                  "version": "1.1.0",
                  "error": "Payload too large",
                  "code": "payload_too_large",
                  "message": "Payload too large",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-payload_too_large"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "ok": false,
                  "status": "error",
                  "code": "rate_limited",
                  "message": "Too many requests.",
                  "hint": "Wait the Retry-After seconds; the RateLimit headers show the policy.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-rate_limited",
                  "error": "Too many requests"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            }
          },
          "503": {
            "description": "Unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "unavailable",
                  "status": "error",
                  "version": "1.1.0",
                  "error": "Image storage is not available",
                  "message": "Image storage is not available",
                  "hint": "Retry later.",
                  "docs": "https://necto-consulting.pages.dev/developers/#error-unavailable"
                }
              }
            }
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "operationId": "mcp",
        "summary": "MCP server, JSON-RPC 2.0 over POST",
        "description": "MCP server, JSON-RPC 2.0 over POST: read-only tools (tools/list names them). MCP 2026-07-28, each request on its own (params._meta and the MCP-Protocol-Version, Mcp-Method and Mcp-Name headers; server/discover lists the versions), and MCP 2025-06-18 (initialize first).",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/McpProtocolVersion"
          },
          {
            "$ref": "#/components/parameters/McpMethod"
          },
          {
            "$ref": "#/components/parameters/McpName"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JsonRpc"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpc"
                }
              }
            }
          },
          "202": {
            "description": "Accepted (a notification)"
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpc"
                },
                "example": {
                  "jsonrpc": "2.0",
                  "id": null,
                  "error": {
                    "code": -32700,
                    "message": "Parse error"
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonRpc"
                },
                "example": {
                  "jsonrpc": "2.0",
                  "id": 1,
                  "error": {
                    "code": -32601,
                    "message": "Method not found: ping"
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                },
                "example": {
                  "error": "rate_limited",
                  "error_description": "Too many requests: retry after 60 seconds."
                }
              }
            },
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              }
            }
          }
        }
      }
    }
  }
}
