{
  "openapi": "3.1.0",
  "info": {
    "title": "Johannes Homeier - public site API",
    "version": "1.0.0",
    "summary": "The single public endpoint of johanneshomeier.com: leave a note.",
    "description": "This personal site exposes one write endpoint. It accepts a short note and an optional reply-to address, and delivers it to the site owner. There is no authentication and no read API; the site content itself is available at /llms.txt, /llms-full.txt and /index.md. The endpoint is readable from any origin: it answers the CORS preflight and returns `Access-Control-Allow-Origin: *`, so a browser-based agent can call it directly.",
    "contact": {
      "name": "Johannes Homeier",
      "email": "hello@johanneshomeier.com",
      "url": "https://johanneshomeier.com"
    },
    "license": {
      "name": "All rights reserved",
      "url": "https://johanneshomeier.com/imprint/"
    }
  },
  "servers": [
    {
      "url": "https://johanneshomeier.com",
      "description": "Production"
    }
  ],
  "x-versioning": {
    "policy": "Breaking changes ship under a new path segment (/api/v2/...) rather than altering an existing version in place. Pin to /api/v1/ when integrating; the unversioned /api/contact always follows the newest version and can change without notice.",
    "current": "v1",
    "deprecationSignals": [
      "A retiring version returns a `Deprecation` header and a `Sunset` header (RFC 8594) carrying the retirement date.",
      "A version keeps serving for at least six months after its Sunset header first appears."
    ],
    "documentation": "https://johanneshomeier.com/docs/"
  },
  "x-rateLimit": {
    "limit": 10,
    "windowSeconds": 3600,
    "scope": "Enforced per serving instance rather than globally, so treat it as a floor rather than a guarantee.",
    "headers": [
      "RateLimit-Limit",
      "RateLimit-Remaining",
      "RateLimit-Reset",
      "RateLimit-Policy",
      "Retry-After"
    ]
  },
  "paths": {
    "/api/contact": {
      "post": {
        "operationId": "leaveNote",
        "summary": "Leave a note for Johannes Homeier",
        "description": "Delivers a short message to the site owner. A reply-to email address is optional. When the site is configured with a Friendly Captcha key, `frcCaptchaResponse` is required and must carry a valid widget token; the public sitekey is exposed on the home page.",
        "tags": [
          "contact"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NoteRequest"
              },
              "examples": {
                "minimal": {
                  "summary": "Note only",
                  "value": {
                    "note": "Enjoyed the write-up on signal-based composables."
                  }
                },
                "withReplyTo": {
                  "summary": "Note with a reply-to address",
                  "value": {
                    "note": "Would you be open to a short consulting engagement?",
                    "email": "someone@example.com"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The note was stored, emailed, or both.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests permitted in the current window.",
                "schema": {
                  "type": "integer",
                  "example": 10
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests still available in the current window.",
                "schema": {
                  "type": "integer",
                  "example": 9
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the current window resets.",
                "schema": {
                  "type": "integer",
                  "example": 3600
                }
              },
              "RateLimit-Policy": {
                "description": "The policy in force, as `<limit>;w=<window-seconds>`.",
                "schema": {
                  "type": "string",
                  "example": "10;w=3600"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NoteAccepted"
                }
              }
            }
          },
          "400": {
            "description": "The body was not valid JSON, the note was empty or longer than 5000 characters, the email address was malformed, or captcha verification failed. See `code` for the specific reason.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests permitted in the current window.",
                "schema": {
                  "type": "integer",
                  "example": 10
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests still available in the current window.",
                "schema": {
                  "type": "integer",
                  "example": 9
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the current window resets.",
                "schema": {
                  "type": "integer",
                  "example": 3600
                }
              },
              "RateLimit-Policy": {
                "description": "The policy in force, as `<limit>;w=<window-seconds>`.",
                "schema": {
                  "type": "string",
                  "example": "10;w=3600"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. `Retry-After` and the RateLimit headers say when to try again.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests permitted in the current window.",
                "schema": {
                  "type": "integer",
                  "example": 10
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests still available in the current window.",
                "schema": {
                  "type": "integer",
                  "example": 9
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the current window resets.",
                "schema": {
                  "type": "integer",
                  "example": 3600
                }
              },
              "RateLimit-Policy": {
                "description": "The policy in force, as `<limit>;w=<window-seconds>`.",
                "schema": {
                  "type": "string",
                  "example": "10;w=3600"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "example": 3600
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Contact delivery is not configured on the server.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests permitted in the current window.",
                "schema": {
                  "type": "integer",
                  "example": 10
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests still available in the current window.",
                "schema": {
                  "type": "integer",
                  "example": 9
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the current window resets.",
                "schema": {
                  "type": "integer",
                  "example": 3600
                }
              },
              "RateLimit-Policy": {
                "description": "The policy in force, as `<limit>;w=<window-seconds>`.",
                "schema": {
                  "type": "string",
                  "example": "10;w=3600"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Both delivery sinks failed; the note was not saved. Safe to retry.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests permitted in the current window.",
                "schema": {
                  "type": "integer",
                  "example": 10
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests still available in the current window.",
                "schema": {
                  "type": "integer",
                  "example": 9
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the current window resets.",
                "schema": {
                  "type": "integer",
                  "example": 3600
                }
              },
              "RateLimit-Policy": {
                "description": "The policy in force, as `<limit>;w=<window-seconds>`.",
                "schema": {
                  "type": "string",
                  "example": "10;w=3600"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/contact": {
      "post": {
        "operationId": "leaveNoteV1",
        "summary": "Leave a note (version-pinned)",
        "description": "Identical to POST /api/contact, pinned to v1. Prefer this path when integrating: a breaking change will ship as /api/v2/contact rather than changing this one.",
        "tags": [
          "contact"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NoteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The note was stored, emailed, or both.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests permitted in the current window.",
                "schema": {
                  "type": "integer",
                  "example": 10
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests still available in the current window.",
                "schema": {
                  "type": "integer",
                  "example": 9
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the current window resets.",
                "schema": {
                  "type": "integer",
                  "example": 3600
                }
              },
              "RateLimit-Policy": {
                "description": "The policy in force, as `<limit>;w=<window-seconds>`.",
                "schema": {
                  "type": "string",
                  "example": "10;w=3600"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NoteAccepted"
                }
              }
            }
          },
          "400": {
            "description": "The body was not valid JSON, the note was empty or longer than 5000 characters, the email address was malformed, or captcha verification failed. See `code` for the specific reason.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests permitted in the current window.",
                "schema": {
                  "type": "integer",
                  "example": 10
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests still available in the current window.",
                "schema": {
                  "type": "integer",
                  "example": 9
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the current window resets.",
                "schema": {
                  "type": "integer",
                  "example": 3600
                }
              },
              "RateLimit-Policy": {
                "description": "The policy in force, as `<limit>;w=<window-seconds>`.",
                "schema": {
                  "type": "string",
                  "example": "10;w=3600"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. `Retry-After` and the RateLimit headers say when to try again.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests permitted in the current window.",
                "schema": {
                  "type": "integer",
                  "example": 10
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests still available in the current window.",
                "schema": {
                  "type": "integer",
                  "example": 9
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the current window resets.",
                "schema": {
                  "type": "integer",
                  "example": 3600
                }
              },
              "RateLimit-Policy": {
                "description": "The policy in force, as `<limit>;w=<window-seconds>`.",
                "schema": {
                  "type": "string",
                  "example": "10;w=3600"
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "example": 3600
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Contact delivery is not configured on the server.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests permitted in the current window.",
                "schema": {
                  "type": "integer",
                  "example": 10
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests still available in the current window.",
                "schema": {
                  "type": "integer",
                  "example": 9
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the current window resets.",
                "schema": {
                  "type": "integer",
                  "example": 3600
                }
              },
              "RateLimit-Policy": {
                "description": "The policy in force, as `<limit>;w=<window-seconds>`.",
                "schema": {
                  "type": "string",
                  "example": "10;w=3600"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Both delivery sinks failed; the note was not saved. Safe to retry.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests permitted in the current window.",
                "schema": {
                  "type": "integer",
                  "example": 10
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests still available in the current window.",
                "schema": {
                  "type": "integer",
                  "example": 9
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the current window resets.",
                "schema": {
                  "type": "integer",
                  "example": 3600
                }
              },
              "RateLimit-Policy": {
                "description": "The policy in force, as `<limit>;w=<window-seconds>`.",
                "schema": {
                  "type": "string",
                  "example": "10;w=3600"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "NoteRequest": {
        "type": "object",
        "required": [
          "note"
        ],
        "additionalProperties": false,
        "properties": {
          "note": {
            "type": "string",
            "minLength": 1,
            "maxLength": 5000,
            "description": "The message. Control characters other than tab and newline are stripped."
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254,
            "description": "Optional reply-to address. Omit it to stay anonymous."
          },
          "frcCaptchaResponse": {
            "type": "string",
            "description": "Friendly Captcha widget token. Required when the site runs with captcha enabled."
          }
        }
      },
      "NoteAccepted": {
        "type": "object",
        "required": [
          "ok"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "code",
          "resolution",
          "status"
        ],
        "description": "Every error from this API uses this shape - never an HTML page.",
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable reason."
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable identifier; branch on this, not on the message.",
            "enum": [
              "invalid_body",
              "note_empty",
              "note_too_long",
              "email_invalid",
              "captcha_required",
              "captcha_failed",
              "rate_limited",
              "method_not_allowed",
              "endpoint_not_found",
              "not_configured",
              "delivery_failed"
            ]
          },
          "resolution": {
            "type": "string",
            "description": "What the caller should do next."
          },
          "status": {
            "type": "integer",
            "description": "Repeats the HTTP status, for logging."
          }
        }
      }
    }
  }
}