{
  "openapi": "3.1.0",
  "info": {
    "title": "RRM Academy Agent API",
    "version": "1.0.0",
    "description": "Programmatic access to the RRM Academy research library and editorial guardrails, exposed via the Model Context Protocol (MCP). 4,050+ peer-reviewed articles on restorative reproductive medicine, NaProTechnology, fertility awareness-based methods, endometriosis, PCOS, and related women's health topics. Clinical content curated under the direction of Dr. Naomi Whittaker, MD.",
    "contact": {
      "name": "RRM Academy",
      "email": "info@rrmacademy.org",
      "url": "https://rrmacademy.org"
    },
    "license": {
      "name": "Fair-use citation",
      "url": "https://rrmacademy.org/terms-of-use/"
    }
  },
  "servers": [
    {
      "url": "https://mcp.rrmacademy.org",
      "description": "Apex MCP server (high-level RAG, semantic search, editorial guardrails). Bearer auth, self-service keys at https://rrmacademy.org/account/mcp-keys."
    },
    {
      "url": "https://mcp-library.rrmacademy.org",
      "description": "Library MCP server (low-level read-only structured access to the library: status, get_article, search_metadata). Public, no auth."
    },
    {
      "url": "https://rrmacademy.org",
      "description": "RRM Academy Pages (ask endpoint, articles list, articles bulk, sandbox)."
    }
  ],
  "security": [{ "bearerAuth": [] }],
  "x-idempotency-policy": {
    "header": "Idempotency-Key",
    "draft": "https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/",
    "key_format": "16-128 printable ASCII characters; recommend a 36-character UUID v4",
    "ttl_hours": 24,
    "mismatch_status": 422,
    "replay_header": "Idempotency-Replayed: true",
    "applies_to": [
      { "method": "POST",   "path": "/api/ask" },
      { "method": "POST",   "path": "/mcp" },
      { "method": "POST",   "path": "/api/contact/submit" },
      { "method": "POST",   "path": "/api/newsletter/subscribe" },
      { "method": "POST",   "path": "/api/community/posts" },
      { "method": "PATCH",  "path": "/api/community/posts" },
      { "method": "DELETE", "path": "/api/community/posts" },
      { "method": "POST",   "path": "/api/community/comments" },
      { "method": "PATCH",  "path": "/api/community/comments" },
      { "method": "DELETE", "path": "/api/community/comments" },
      { "method": "POST",   "path": "/api/community/reactions" },
      { "method": "DELETE", "path": "/api/community/reactions" },
      { "method": "POST",   "path": "/api/saved" },
      { "method": "DELETE", "path": "/api/saved" }
    ]
  },
  "webhooks": {
    "stripeEvent": {
      "post": {
        "operationId": "stripe_webhook_inbound",
        "summary": "Inbound Stripe event (server-to-server)",
        "description": "Stripe posts billing events to https://rrmacademy.org/api/stripe-webhook . The handler verifies the `Stripe-Signature` header (HMAC-SHA256 over the raw request body, prefixed with a timestamp, per https://docs.stripe.com/webhooks/signatures) before processing. Requests with a missing or invalid signature are rejected with 400. The handler is idempotent: replayed events with the same `id` short-circuit on a dedup KV record. Accepted event types: `checkout.session.completed`, `checkout.session.expired`, `customer.subscription.updated`, `customer.subscription.deleted`, `invoice.payment_failed`, `charge.refunded`. This is the only inbound webhook surface; rrmacademy.org does not currently emit outbound webhooks to consumers.",
        "servers": [{ "url": "https://rrmacademy.org" }],
        "parameters": [
          {
            "name": "Stripe-Signature",
            "in": "header",
            "required": true,
            "description": "Stripe HMAC-SHA256 signature over the raw POST body. Format: `t=<unix_ts>,v1=<hex_hmac>`. Verified by `stripe.webhooks.constructEventAsync` using the rotating webhook signing secret. See https://docs.stripe.com/webhooks/signatures.",
            "schema": {
              "type": "string",
              "example": "t=1701234567,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/StripeWebhookEvent" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Event accepted (verified, dispatched to the matching handler, or idempotently skipped)."
          },
          "400": {
            "description": "Missing or malformed `Stripe-Signature` header, malformed JSON body, or signature failed HMAC verification.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
            }
          },
          "500": {
            "description": "Internal error while processing the event. Stripe will retry per its retry policy.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
            }
          }
        }
      }
    }
  },
  "paths": {
    "/api/ask": {
      "get": {
        "operationId": "ask_capability",
        "summary": "NLWeb capability metadata",
        "description": "Returns machine-readable capability JSON describing the /api/ask endpoint: supported methods, auth model, streaming transport, request/response shape, and guardrails. Unauthenticated. Cacheable for 1 hour.",
        "security": [],
        "servers": [{ "url": "https://rrmacademy.org" }],
        "responses": {
          "200": {
            "description": "Capability descriptor",
            "headers": {
              "Cache-Control": {
                "schema": { "type": "string" },
                "description": "public, max-age=3600"
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AskCapability" }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "ask_query",
        "summary": "Conversational AI over the RRM Research Library",
        "description": "Answers questions scoped to restorative reproductive medicine using the RRM Library corpus. Two auth paths:\n\n**Session-authenticated (20 req/day):** Include the `session` cookie from `/api/auth/login`. Supports both JSON and SSE response formats based on the `Accept` header.\n\n**Anonymous (2 req/day/IP):** No cookie required. Always returns `text/event-stream`. Rate-limited per IP.\n\nSet `Accept: text/event-stream` to receive a streaming SSE response regardless of auth state.",
        "security": [],
        "servers": [{ "url": "https://rrmacademy.org" }],
        "parameters": [
          { "$ref": "#/components/parameters/IdempotencyKey" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["message"],
                "properties": {
                  "message": {
                    "type": "string",
                    "minLength": 2,
                    "maxLength": 500,
                    "description": "The question to ask the RRM Library AI"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Answer with citations. Content-Type is `application/json` for authenticated requests without SSE Accept header, or `text/event-stream` for anonymous requests and any SSE-accepting request.",
            "headers": {
              "RateLimit-Limit":     { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset":     { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AskResponse" }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "SSE stream. Two events: `data: <AskResponse JSON>` then `data: [DONE]`"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input (message missing, wrong type, or out of range)",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Session path: 20/day reset at midnight UTC. Anonymous path: 2/day, 48h TTL.",
            "headers": {
              "RateLimit-Limit":     { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset":     { "$ref": "#/components/headers/RateLimitReset" },
              "Retry-After": {
                "description": "RFC 7231 -- seconds until the client may retry.",
                "schema": { "type": "integer", "minimum": 0 },
                "example": 60
              }
            },
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
            }
          },
          "503": {
            "description": "Upstream service unavailable",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
            }
          },
          "422": { "$ref": "#/components/responses/IdempotencyMismatch" },
          "400": { "$ref": "#/components/responses/InvalidIdempotencyKey" }
        }
      }
    },
    "/api/articles": {
      "get": {
        "operationId": "list_articles",
        "summary": "List published library articles",
        "description": "Returns a paginated list of published, non-retracted research articles from the RRM Academy library. Public, unauthenticated. Rate limited to 30 requests per minute per IP. Responses are edge-cached for 1 hour.",
        "security": [],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "Page number (1-based). Default: 1. Max: 350.",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 350, "default": 1 }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Results per page. Default: 25. Max: 50.",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 25 }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated article list.",
            "headers": {
              "RateLimit-Limit":     { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset":     { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ArticleList" }
              }
            }
          },
          "400": {
            "description": "Invalid pagination parameters or malformed `ids` query.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "invalid_pagination" }
              }
            }
          },
          "429": {
            "description": "Rate limited (30 req/min per IP)",
            "headers": {
              "RateLimit-Limit":     { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset":     { "$ref": "#/components/headers/RateLimitReset" },
              "Retry-After": {
                "description": "RFC 7231 -- seconds until the client may retry.",
                "schema": { "type": "integer", "minimum": 0 },
                "example": 60
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "rate_limited" }
              }
            }
          },
          "503": {
            "description": "Upstream library service unavailable",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "service_unavailable" }
              }
            }
          }
        }
      }
    },
    "/api/articles/bulk": {
      "get": {
        "operationId": "bulk_articles",
        "summary": "Bulk-fetch articles by ID",
        "description": "Returns up to 50 published library articles by ID in a single call. Comma-separated IDs in the `ids` query parameter. Missing IDs are returned in `not_found[]` instead of producing a 404. Shares the 30 req/min IP budget with /api/articles.",
        "security": [],
        "parameters": [
          {
            "name": "ids",
            "in": "query",
            "required": true,
            "description": "Comma-separated list of article IDs to fetch (max 50, deduped, order preserved).",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9-]+(,[A-Za-z0-9-]+){0,49}$",
              "example": "rec123abc,rec456def,rec789ghi"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Bulk article results. Missing IDs appear in not_found[].",
            "headers": {
              "RateLimit-Limit":     { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset":     { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/BulkArticleResponse" }
              }
            }
          },
          "400": {
            "description": "Missing or malformed ids parameter.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "invalid_ids" }
              }
            }
          },
          "429": {
            "description": "Rate limited (30 req/min per IP, shared with /api/articles)",
            "headers": {
              "RateLimit-Limit":     { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset":     { "$ref": "#/components/headers/RateLimitReset" },
              "Retry-After": {
                "description": "RFC 7231 -- seconds until the client may retry.",
                "schema": { "type": "integer", "minimum": 0 },
                "example": 60
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "rate_limited" }
              }
            }
          },
          "503": {
            "description": "Upstream library service unavailable",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "service_unavailable" }
              }
            }
          }
        }
      }
    },
    "/api/bulk": {
      "get": {
        "operationId": "bulk_articles_alias",
        "summary": "Bulk-fetch articles by ID (top-level alias)",
        "description": "Top-level alias for /api/articles/bulk. Identical behavior. Shares the 30 req/min IP budget. Exists so URL-pattern scanners recognize the batch endpoint at /api/bulk.",
        "security": [],
        "parameters": [
          {
            "name": "ids",
            "in": "query",
            "description": "Comma-separated list of article IDs to bulk-fetch (max 50, deduped, order preserved). Missing IDs returned in not_found[] instead of producing a 404.",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9-]+(,[A-Za-z0-9-]+){0,49}$",
              "example": "rec123abc,rec456def,rec789ghi"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Bulk-fetch results (BulkArticleResponse shape)",
            "headers": {
              "RateLimit-Limit":     { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset":     { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/BulkArticleResponse" }
              }
            }
          },
          "400": {
            "description": "Malformed `ids` query (regex mismatch).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "invalid_ids" }
              }
            }
          },
          "429": {
            "description": "Rate limited (30 req/min per IP)",
            "headers": {
              "RateLimit-Limit":     { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset":     { "$ref": "#/components/headers/RateLimitReset" },
              "Retry-After": {
                "description": "RFC 7231 -- seconds until the client may retry.",
                "schema": { "type": "integer", "minimum": 0 },
                "example": 60
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "503": {
            "description": "Upstream library service unavailable",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/api/ask/sandbox": {
      "post": {
        "operationId": "ask_sandbox",
        "summary": "Canned sandbox response for integration testing",
        "description": "Returns a canned response with no LLM invocation, no auth, and no rate limit. Accepts the same JSON shape as /api/ask but ignores the body. Use to validate that your client correctly parses the AskResponse-shaped reply before integrating with the live /api/ask endpoint.",
        "security": [],
        "servers": [{ "url": "https://rrmacademy.org" }],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "message": {
                    "type": "string",
                    "description": "Ignored. Included for structural compatibility with /api/ask."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Canned sandbox response. Shape matches SandboxAskResponse.",
            "headers": {
              "X-Sandbox": {
                "description": "Always `true` on this endpoint.",
                "schema": { "type": "string", "enum": ["true"] }
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SandboxAskResponse" }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "SSE stream when Accept: text/event-stream. Two events: `data: <SandboxAskResponse JSON>` then `data: [DONE]`"
                }
              }
            }
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "operationId": "mcp_invoke",
        "summary": "Invoke an MCP tool",
        "description": "Model Context Protocol JSON-RPC 2.0 entry point. Dispatches to one of five tools: search, check_guardrails, check_facts, get_article, find_related. Use the MCP client libraries (@modelcontextprotocol/sdk) for idiomatic access rather than raw HTTP.",
        "parameters": [
          { "$ref": "#/components/parameters/IdempotencyKey" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/JsonRpcRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response envelope",
            "headers": {
              "RateLimit-Limit":     { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset":     { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/JsonRpcResponse" }
              }
            }
          },
          "401": {
            "description": "Missing or invalid bearer token",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "headers": {
              "RateLimit-Limit":     { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset":     { "$ref": "#/components/headers/RateLimitReset" },
              "Retry-After": {
                "description": "RFC 7231 -- seconds until the client may retry.",
                "schema": { "type": "integer", "minimum": 0 },
                "example": 60
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "503": {
            "description": "Upstream unavailable",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "422": { "$ref": "#/components/responses/IdempotencyMismatch" },
          "400": { "$ref": "#/components/responses/InvalidIdempotencyKey" }
        }
      }
    },
    "/health": {
      "get": {
        "operationId": "health_check",
        "summary": "Health check",
        "description": "Returns 200 when the MCP server is reachable. Unauthenticated.",
        "security": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": { "schema": { "$ref": "#/components/schemas/HealthStatus" } }
            }
          }
        }
      }
    },
    "/api/contact/submit": {
      "post": {
        "operationId": "contact_submit",
        "summary": "Submit a contact form message",
        "tags": ["Mutations"],
        "security": [],
        "servers": [{ "url": "https://rrmacademy.org" }],
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "type": "object", "description": "See functions/api/contact/submit.js for the request schema." }
            }
          }
        },
        "responses": {
          "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/InvalidIdempotencyKey" },
          "422": { "$ref": "#/components/responses/IdempotencyMismatch" },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "503": { "description": "Service unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/newsletter/subscribe": {
      "post": {
        "operationId": "newsletter_subscribe",
        "summary": "Subscribe an email address to the newsletter",
        "tags": ["Mutations"],
        "security": [],
        "servers": [{ "url": "https://rrmacademy.org" }],
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "type": "object", "description": "See functions/api/newsletter/subscribe.js for the request schema." }
            }
          }
        },
        "responses": {
          "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/InvalidIdempotencyKey" },
          "422": { "$ref": "#/components/responses/IdempotencyMismatch" },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "503": { "description": "Service unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/community/posts": {
      "post": {
        "operationId": "community_posts_create",
        "summary": "Create a community post",
        "tags": ["Mutations"],
        "security": [],
        "servers": [{ "url": "https://rrmacademy.org" }],
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "description": "See functions/api/community/posts.js for the request schema." } } } },
        "responses": {
          "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/InvalidIdempotencyKey" },
          "401": { "description": "Authentication required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "$ref": "#/components/responses/IdempotencyMismatch" },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "patch": {
        "operationId": "community_posts_update",
        "summary": "Edit a community post the caller authored",
        "tags": ["Mutations"],
        "security": [],
        "servers": [{ "url": "https://rrmacademy.org" }],
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "description": "See functions/api/community/posts.js for the request schema." } } } },
        "responses": {
          "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/InvalidIdempotencyKey" },
          "401": { "description": "Authentication required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Caller does not own the post", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "$ref": "#/components/responses/IdempotencyMismatch" }
        }
      },
      "delete": {
        "operationId": "community_posts_delete",
        "summary": "Delete a community post the caller authored",
        "tags": ["Mutations"],
        "security": [],
        "servers": [{ "url": "https://rrmacademy.org" }],
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "responses": {
          "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/InvalidIdempotencyKey" },
          "401": { "description": "Authentication required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Caller does not own the post", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "$ref": "#/components/responses/IdempotencyMismatch" }
        }
      }
    },
    "/api/community/comments": {
      "post": {
        "operationId": "community_comments_create",
        "summary": "Create a comment on a community post",
        "tags": ["Mutations"],
        "security": [],
        "servers": [{ "url": "https://rrmacademy.org" }],
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "description": "See functions/api/community/comments.js for the request schema." } } } },
        "responses": {
          "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/InvalidIdempotencyKey" },
          "401": { "description": "Authentication required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "$ref": "#/components/responses/IdempotencyMismatch" },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "patch": {
        "operationId": "community_comments_update",
        "summary": "Edit a comment the caller authored",
        "tags": ["Mutations"],
        "security": [],
        "servers": [{ "url": "https://rrmacademy.org" }],
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "description": "See functions/api/community/comments.js for the request schema." } } } },
        "responses": {
          "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/InvalidIdempotencyKey" },
          "401": { "description": "Authentication required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Caller does not own the comment", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "$ref": "#/components/responses/IdempotencyMismatch" }
        }
      },
      "delete": {
        "operationId": "community_comments_delete",
        "summary": "Delete a comment the caller authored",
        "tags": ["Mutations"],
        "security": [],
        "servers": [{ "url": "https://rrmacademy.org" }],
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "responses": {
          "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/InvalidIdempotencyKey" },
          "401": { "description": "Authentication required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Caller does not own the comment", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "$ref": "#/components/responses/IdempotencyMismatch" }
        }
      }
    },
    "/api/community/reactions": {
      "post": {
        "operationId": "community_reactions_create",
        "summary": "Add a reaction to a post or comment",
        "tags": ["Mutations"],
        "security": [],
        "servers": [{ "url": "https://rrmacademy.org" }],
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "description": "See functions/api/community/reactions.js for the request schema." } } } },
        "responses": {
          "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/InvalidIdempotencyKey" },
          "401": { "description": "Authentication required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "$ref": "#/components/responses/IdempotencyMismatch" },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "delete": {
        "operationId": "community_reactions_delete",
        "summary": "Remove a reaction the caller added",
        "tags": ["Mutations"],
        "security": [],
        "servers": [{ "url": "https://rrmacademy.org" }],
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "responses": {
          "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/InvalidIdempotencyKey" },
          "401": { "description": "Authentication required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "$ref": "#/components/responses/IdempotencyMismatch" }
        }
      }
    },
    "/api/saved": {
      "post": {
        "operationId": "saved_create",
        "summary": "Save a library article to the caller's collection",
        "tags": ["Mutations"],
        "security": [],
        "servers": [{ "url": "https://rrmacademy.org" }],
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "description": "See functions/api/saved.js for the request schema." } } } },
        "responses": {
          "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/InvalidIdempotencyKey" },
          "401": { "description": "Authentication required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "$ref": "#/components/responses/IdempotencyMismatch" }
        }
      },
      "delete": {
        "operationId": "saved_delete",
        "summary": "Remove a library article from the caller's collection",
        "tags": ["Mutations"],
        "security": [],
        "servers": [{ "url": "https://rrmacademy.org" }],
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "responses": {
          "200": { "description": "Success", "content": { "application/json": { "schema": { "type": "object" } } } },
          "400": { "$ref": "#/components/responses/InvalidIdempotencyKey" },
          "401": { "description": "Authentication required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "$ref": "#/components/responses/IdempotencyMismatch" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "opaque",
        "description": "Bearer API key. Self-service issuance at https://rrmacademy.org/account/mcp-keys."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Opaque client-generated identifier (recommend 36-character UUID) that lets the server detect retried requests on this mutation endpoint and return the cached prior response instead of re-running side effects. Implements the IETF idempotency-header draft (https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/). Constraints: 16-128 printable ASCII characters. The server returns the same status, body, and Idempotency-Replayed: true on a successful match. If the same key is reused with a different request body, the server returns 422 with error=idempotency-mismatch. Keys expire after 24 hours. Idempotency is opt-in: requests without this header behave exactly as before.",
        "schema": {
          "type": "string",
          "minLength": 16,
          "maxLength": 128,
          "pattern": "^[\\x21-\\x7e]{16,128}$",
          "example": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
        }
      }
    },
    "headers": {
      "RateLimitLimit": {
        "description": "RFC 9598 (draft-ietf-httpapi-ratelimit-headers) -- total requests allowed in the current window.",
        "schema": { "type": "integer", "minimum": 0 },
        "example": 20
      },
      "RateLimitRemaining": {
        "description": "RFC 9598 -- requests remaining in the current window. Agents should self-throttle when this drops to 0 rather than retrying through a 429.",
        "schema": { "type": "integer", "minimum": 0 },
        "example": 18
      },
      "RateLimitReset": {
        "description": "RFC 9598 -- seconds until the current window resets.",
        "schema": { "type": "integer", "minimum": 0 },
        "example": 60
      }
    },
    "responses": {
      "IdempotencyMismatch": {
        "description": "The Idempotency-Key was previously used with a different request body. Use a new key for a new request.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "ok": false, "error": "idempotency-mismatch", "message": "This Idempotency-Key was used with a different request body. Use a new key for a new request." }
          }
        }
      },
      "InvalidIdempotencyKey": {
        "description": "Idempotency-Key header is malformed. Must be 16-128 printable ASCII characters.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "ok": false, "error": "invalid-idempotency-key", "message": "Idempotency-Key must be 16-128 printable ASCII characters (RFC IETF idempotency-header draft)." }
          }
        }
      }
    },
    "schemas": {
      "JsonRpcRequest": {
        "type": "object",
        "required": ["jsonrpc", "method", "id"],
        "properties": {
          "jsonrpc": { "type": "string", "const": "2.0" },
          "method": {
            "type": "string",
            "enum": ["tools/list", "tools/call", "initialize"]
          },
          "params": {
            "type": "object",
            "description": "For tools/call: { name: string, arguments: object }. Tool names: search, check_guardrails, check_facts, get_article, find_related."
          },
          "id": { "oneOf": [{ "type": "string" }, { "type": "number" }] }
        }
      },
      "JsonRpcResponse": {
        "type": "object",
        "required": ["jsonrpc", "id"],
        "properties": {
          "jsonrpc": { "type": "string", "const": "2.0" },
          "id": { "oneOf": [{ "type": "string" }, { "type": "number" }] },
          "result": { "type": "object" },
          "error": {
            "type": "object",
            "properties": {
              "code": { "type": "integer" },
              "message": { "type": "string" },
              "data": {}
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string", "description": "Machine-readable error code" },
          "message": { "type": "string", "description": "Human-readable error detail" }
        },
        "required": ["error"]
      },
      "AskResponse": {
        "type": "object",
        "required": ["answer", "citations"],
        "properties": {
          "answer": { "type": "string", "description": "AI-generated answer scoped to RRM Library content" },
          "citations": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["url"],
              "properties": {
                "url": { "type": "string", "format": "uri" },
                "title": { "type": "string" }
              }
            }
          },
          "fallback": { "type": "boolean", "description": "true when no relevant library content was found and a fallback message is returned" }
        }
      },
      "AskCapability": {
        "type": "object",
        "properties": {
          "endpoint": { "type": "string", "example": "/api/ask" },
          "methods": { "type": "array", "items": { "type": "string" }, "example": ["GET", "POST"] },
          "auth": { "type": "object" },
          "streaming": { "type": "object" },
          "request": { "type": "object" },
          "response": { "type": "object" },
          "guardrails": { "type": "object" },
          "site": { "type": "string", "format": "uri" },
          "library": { "type": "string", "format": "uri" }
        }
      },
      "Citation": {
        "type": "object",
        "required": ["url"],
        "properties": {
          "url": { "type": "string", "format": "uri" },
          "title": { "type": "string" }
        }
      },
      "ArticleSummary": {
        "type": "object",
        "required": ["id", "slug", "url", "title"],
        "properties": {
          "id": { "type": "string", "example": "rec123abc" },
          "slug": { "type": "string", "example": "napro-endometriosis-2022" },
          "url": { "type": "string", "format": "uri", "example": "https://rrmacademy.org/library/napro-endometriosis-2022/" },
          "title": { "type": "string" },
          "authors": { "type": "string" },
          "year": { "type": "integer", "nullable": true },
          "journal": { "type": "string" },
          "doi": { "type": "string" },
          "pmid": { "type": "string" },
          "abstract": { "type": "string" },
          "topics": { "type": "array", "items": { "type": "string" } },
          "is_open_access": { "type": "boolean" },
          "date_added": { "type": "string", "format": "date", "nullable": true, "example": "2026-04-19" }
        }
      },
      "ArticleList": {
        "type": "object",
        "required": ["page", "limit", "total", "total_pages", "results"],
        "properties": {
          "page": { "type": "integer", "example": 1 },
          "limit": { "type": "integer", "example": 25 },
          "total": { "type": "integer", "example": 3467 },
          "total_pages": { "type": "integer", "example": 139 },
          "results": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/ArticleSummary" }
          }
        }
      },
      "BulkArticleResponse": {
        "type": "object",
        "required": ["results", "not_found", "requested", "returned"],
        "description": "Returned by GET /api/articles/bulk. Results for found IDs plus a not_found[] list for missing ones.",
        "properties": {
          "results": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/ArticleSummary" }
          },
          "not_found": {
            "type": "array",
            "items": { "type": "string", "description": "Requested ID that did not resolve to a published article" },
            "example": ["rec999missing"]
          },
          "requested": { "type": "integer", "example": 3 },
          "returned": { "type": "integer", "example": 2 }
        }
      },
      "HealthStatus": {
        "type": "string",
        "enum": ["ok"],
        "description": "Health probe response. Returns the literal string `ok` when the worker is reachable.",
        "example": "ok"
      },
      "StripeWebhookEvent": {
        "type": "object",
        "required": ["id", "type", "data"],
        "description": "Subset of the Stripe Event object delivered to /api/stripe-webhook. Full schema: https://docs.stripe.com/api/events/object.",
        "properties": {
          "id": { "type": "string", "example": "evt_1NkFh82eZvKYlo2C0w6cVHaJ" },
          "type": {
            "type": "string",
            "description": "Stripe event type. Accepted types: checkout.session.completed, checkout.session.expired, customer.subscription.updated, customer.subscription.deleted, invoice.payment_failed, charge.refunded.",
            "example": "checkout.session.completed"
          },
          "data": {
            "type": "object",
            "description": "Event payload. Shape varies by event type."
          },
          "livemode": { "type": "boolean" },
          "created": { "type": "integer", "format": "int64" }
        }
      },
      "SandboxAskResponse": {
        "type": "object",
        "required": ["answer", "citations", "sandbox"],
        "description": "Returned by /api/ask when ?mode=sandbox is set. Canned response, does not consume rate-limit budget or invoke the upstream LLM.",
        "properties": {
          "sandbox": { "type": "boolean", "const": true },
          "answer": { "type": "string", "example": "Sandbox response. /api/ask returns answers grounded in the RRM Library." },
          "citations": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Citation" }
          }
        }
      }
    }
  }
}
