{
  "openapi": "3.1.0",
  "info": {
    "title": "Arne Kellmann Website API",
    "version": "1.0.0",
    "description": "Public keyless read API v1 and existing contact form. Breaking changes use a new major URL path. Planned removals receive at least 90 days notice in /developers with Deprecation and Sunset headers. No removal scheduled.",
    "contact": {
      "name": "Arne Kellmann",
      "email": "contact@arnekellmann.de",
      "url": "https://arnekellmann.de"
    }
  },
  "servers": [
    {
      "url": "https://arnekellmann.de",
      "description": "Production server"
    }
  ],
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "success",
          "code"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "code": {
            "type": "string"
          },
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "errors": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        }
      }
    },
    "headers": {
      "Deprecation": {
        "description": "Sent when an endpoint is scheduled for retirement; no retirement is currently scheduled.",
        "schema": {
          "type": "string"
        }
      },
      "Sunset": {
        "description": "Retirement date at least 90 days after notice.",
        "schema": {
          "type": "string"
        }
      }
    }
  },
  "security": [],
  "externalDocs": {
    "url": "https://arnekellmann.de/developers"
  },
  "paths": {
    "/api/v1/documents/batch": {
      "post": {
        "operationId": "readDocumentsBatch",
        "summary": "Read up to ten public documents in one request",
        "description": "Read-only, synchronous, no authentication. Maximum 4096 UTF-8 request bytes. Local paths only; no external fetching. Results preserve order and duplicates. Public read quota applies to each batch request.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "paths"
                ],
                "properties": {
                  "paths": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 10,
                    "items": {
                      "type": "string",
                      "maxLength": 256,
                      "pattern": "^/(?!/)[A-Za-z0-9/_.-]*$",
                      "description": "Local document path; dot and dotdot segments are rejected.",
                      "examples": [
                        "/pricing.md",
                        "/auth.md"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-document outcomes; missing documents do not fail the batch.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "results"
                  ],
                  "properties": {
                    "results": {
                      "type": "array",
                      "maxItems": 10,
                      "items": {
                        "oneOf": [
                          {
                            "type": "object",
                            "required": [
                              "path",
                              "status",
                              "code",
                              "text"
                            ],
                            "properties": {
                              "path": {
                                "type": "string"
                              },
                              "status": {
                                "const": 200
                              },
                              "code": {
                                "const": "OK"
                              },
                              "text": {
                                "type": "string"
                              }
                            }
                          },
                          {
                            "type": "object",
                            "required": [
                              "path",
                              "status",
                              "code",
                              "text"
                            ],
                            "properties": {
                              "path": {
                                "type": "string"
                              },
                              "status": {
                                "const": 404
                              },
                              "code": {
                                "const": "NOT_FOUND"
                              },
                              "text": {
                                "type": "null"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_BODY",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "PAYLOAD_TOO_LARGE",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "UNSUPPORTED_MEDIA_TYPE",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED; honor Retry-After",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/versioning.md": {
      "get": {
        "operationId": "readVersioningPolicy",
        "summary": "Read versioning, deprecation and sunset policy",
        "responses": {
          "200": {
            "description": "At least 90 days notice before removal; no current deprecation.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/sandbox/v1/services": {
      "get": {
        "operationId": "listSandboxServices",
        "summary": "Exercise the public read-only catalog without credentials or side effects",
        "description": "Same public catalog as production. No writes, delivery, bookings or paid operations are exposed.",
        "security": [],
        "parameters": [
          {
            "name": "language",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "de"
              ],
              "default": "en"
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/paths/~1api~1v1~1services/get/responses/200"
          },
          "400": {
            "description": "Invalid query",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Read quota exceeded; honor Retry-After",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/ask": {
      "get": {
        "operationId": "searchSite",
        "summary": "Search site documentation in NLWeb list mode",
        "parameters": [
          {
            "name": "query",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          },
          {
            "name": "language",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "de"
              ],
              "default": "en"
            }
          },
          {
            "name": "mode",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "const": "list",
              "default": "list"
            }
          },
          {
            "name": "streaming",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "name": "query_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Keyword search of published text. List mode only; SSE emits start, result and complete.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "query_id",
                    "_meta",
                    "results"
                  ],
                  "properties": {
                    "query_id": {
                      "type": "string"
                    },
                    "_meta": {
                      "type": "object",
                      "properties": {
                        "response_type": {
                          "type": "string",
                          "const": "list"
                        },
                        "version": {
                          "type": "string"
                        }
                      }
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "url": {
                            "type": "string",
                            "format": "uri"
                          },
                          "name": {
                            "type": "string"
                          },
                          "site": {
                            "type": "string"
                          },
                          "score": {
                            "type": "number"
                          },
                          "description": {
                            "type": "string"
                          },
                          "schema_object": {
                            "type": "object",
                            "additionalProperties": true
                          }
                        }
                      }
                    }
                  }
                }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Invalid query or unsupported mode",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Query body exceeds 4096 bytes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "POST requires application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "searchSitePost",
        "summary": "Search site documentation with JSON",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "query"
                ],
                "additionalProperties": false,
                "properties": {
                  "query": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "language": {
                    "type": "string",
                    "enum": [
                      "en",
                      "de"
                    ],
                    "default": "en"
                  },
                  "mode": {
                    "type": "string",
                    "const": "list",
                    "default": "list"
                  },
                  "streaming": {
                    "type": "boolean",
                    "default": true
                  },
                  "query_id": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 100
                  },
                  "prefer": {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "streaming"
                    ],
                    "properties": {
                      "streaming": {
                        "type": "boolean"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Keyword search of published text. List mode only; SSE emits start, result and complete.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "query_id",
                    "_meta",
                    "results"
                  ],
                  "properties": {
                    "query_id": {
                      "type": "string"
                    },
                    "_meta": {
                      "type": "object",
                      "properties": {
                        "response_type": {
                          "type": "string",
                          "const": "list"
                        },
                        "version": {
                          "type": "string"
                        }
                      }
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "url": {
                            "type": "string",
                            "format": "uri"
                          },
                          "name": {
                            "type": "string"
                          },
                          "site": {
                            "type": "string"
                          },
                          "score": {
                            "type": "number"
                          },
                          "description": {
                            "type": "string"
                          },
                          "schema_object": {
                            "type": "object",
                            "additionalProperties": true
                          }
                        }
                      }
                    }
                  }
                }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Invalid query or unsupported mode",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Query body exceeds 4096 bytes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "POST requires application/json",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/services": {
      "get": {
        "operationId": "listServices",
        "summary": "Read published consulting services",
        "description": "Complete small catalog; no pagination, account or side effects. Prices require an individual quote.",
        "parameters": [
          {
            "name": "language",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "de"
              ],
              "default": "en"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Published services; anonymous access, no Authorization header.",
            "headers": {
              "RateLimit": {
                "description": "Structured read quota: remaining requests r and reset seconds t.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Policy": {
                "description": "Best-effort per-IP, per-isolate quota of 120 API read requests per 60 seconds.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until reset.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "language",
                    "services",
                    "pricing",
                    "contactUrl"
                  ],
                  "properties": {
                    "language": {
                      "type": "string",
                      "enum": [
                        "en",
                        "de"
                      ]
                    },
                    "contactUrl": {
                      "type": "string",
                      "format": "uri"
                    },
                    "services": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "description",
                          "url"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string",
                            "format": "uri"
                          }
                        }
                      }
                    },
                    "pricing": {
                      "type": "object",
                      "required": [
                        "type",
                        "publishedRates",
                        "hourlyRate",
                        "currency",
                        "unit",
                        "vatIncluded",
                        "url"
                      ],
                      "properties": {
                        "type": {
                          "type": "string",
                          "const": "custom_quote"
                        },
                        "publishedRates": {
                          "type": "boolean",
                          "const": true
                        },
                        "hourlyRate": {
                          "type": "number",
                          "const": 120
                        },
                        "currency": {
                          "type": "string",
                          "const": "EUR"
                        },
                        "unit": {
                          "type": "string",
                          "const": "hour"
                        },
                        "vatIncluded": {
                          "type": "boolean",
                          "const": false
                        },
                        "url": {
                          "type": "string",
                          "format": "uri"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unsupported query parameter or language",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/contact": {
      "post": {
        "summary": "Submit contact form",
        "description": "Sends an email. Requires explicit privacy consent and a Turnstile token when bot protection is configured. Five attempts/minute/IP/Worker isolate (best effort). Optional Idempotency-Key prevents duplicate delivery through Resend for 24 hours. Retries need identical fields and a fresh Turnstile token; do not automatically retry without the original key.",
        "operationId": "submitContact",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Unique key per submission. Reuse for identical retries within 24 hours; fresh Turnstile token required. Conflicting or concurrent use returns 409.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{16,128}$",
              "minLength": 16,
              "maxLength": 128
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "email",
                  "subject",
                  "message",
                  "gdprConsent"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 100
                  },
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "subject": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "message": {
                    "type": "string",
                    "minLength": 10,
                    "maxLength": 5000
                  },
                  "gdprConsent": {
                    "type": "boolean",
                    "const": true
                  },
                  "turnstileToken": {
                    "type": "string"
                  },
                  "language": {
                    "type": "string",
                    "enum": [
                      "en",
                      "de"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Message sent successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON, fields or bot verification",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "IDEMPOTENCY_CONFLICT or IDEMPOTENCY_IN_PROGRESS",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds until retry",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until reset",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "Delivery failed; outcome may be uncertain",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Email delivery not configured; nothing sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "summary": "Health check",
        "description": "Check if the API is running",
        "operationId": "healthCheck",
        "responses": {
          "200": {
            "description": "API is healthy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string"
                    },
                    "timestamp": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/openapi.json": {
      "get": {
        "summary": "OpenAPI specification",
        "description": "Get the OpenAPI specification for this API",
        "operationId": "getOpenApiSpec",
        "responses": {
          "200": {
            "description": "OpenAPI specification",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  }
}