{
  "openapi": "3.1.0",
  "info": {
    "title": "Matthews Wong API",
    "version": "1.0.0",
    "summary": "Ask the site's assistant about Matthews Wong, or send him a message.",
    "description": "The public endpoints behind matthewswong.com: the chat assistant that answers questions about Matthews Wong's work, its follow-up suggestions, and the contact form. Every endpoint is rate limited per client IP. Errors are JSON with a stable `code`, a human-readable `error` and a `hint` on how to recover. Page content is also available as Markdown: request any page with `Accept: text/markdown`.\n\n## Versioning and deprecation\n\nThe version is in the URL path (/api/v1/). Within a version, changes are backwards compatible: fields may be added, never removed or retyped. A breaking change ships as a new version (/api/v2/), and the previous version keeps working for at least 180 days after it is deprecated. Deprecated endpoints answer with a `Deprecation` header (RFC 9745) and a `Link` to their successor; once a removal date is set they also send `Sunset` (RFC 8594), at least 90 days before removal. The unversioned paths /api/chat/, /api/chat/followups/ and /api/contact/ are deprecated aliases of v1.",
    "contact": {
      "name": "Matthews Wong",
      "email": "matthewswong2610@gmail.com",
      "url": "https://www.matthewswong.com/en/contact/"
    }
  },
  "externalDocs": {
    "description": "Developer guide: quickstart, errors, versioning",
    "url": "https://www.matthewswong.com/en/developers/"
  },
  "servers": [
    {
      "url": "https://www.matthewswong.com",
      "description": "Production"
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "Assistant",
      "description": "Questions about Matthews Wong."
    },
    {
      "name": "Contact",
      "description": "Messages to Matthews Wong."
    },
    {
      "name": "Meta",
      "description": "Discovering this API."
    }
  ],
  "paths": {
    "/api/": {
      "get": {
        "operationId": "getApiIndex",
        "tags": [
          "Meta"
        ],
        "summary": "List the API's endpoints",
        "responses": {
          "200": {
            "description": "The endpoints and where this description lives.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiIndex"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/": {
      "get": {
        "operationId": "getApiVersionIndex",
        "tags": [
          "Meta"
        ],
        "summary": "List the v1 endpoints",
        "responses": {
          "200": {
            "description": "The endpoints and where this description lives.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiIndex"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/chat/": {
      "post": {
        "operationId": "askAssistant",
        "tags": [
          "Assistant"
        ],
        "summary": "Ask the assistant about Matthews Wong",
        "description": "Answers questions about Matthews Wong's experience, projects, skills and writing, using retrieval over the site's own data. The answer streams as server-sent events: each `data:` line is JSON, either `{\"delta\": \"...\"}` (the next piece of text), `{\"done\": true}` (end of answer) or `{\"error\": \"rate_limited\" | \"stream_failed\"}`. Limited to 20 requests per minute per client.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChatRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The answer, as a stream of server-sent events.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                },
                "example": "data: {\"delta\":\"Matthews is an AI Forward\"}\n\ndata: {\"delta\":\" Deployed Engineer...\"}\n\ndata: {\"done\":true}\n\n"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/chat/followups/": {
      "post": {
        "operationId": "suggestFollowups",
        "tags": [
          "Assistant"
        ],
        "summary": "Suggest follow-up questions for an answer",
        "description": "Returns up to three short follow-up questions for the assistant's last answer. Never fails: on invalid input, rate limiting or an upstream error it returns an empty list. Limited to 30 requests per minute per client.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FollowupsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Zero to three suggested questions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FollowupsResponse"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          }
        }
      }
    },
    "/api/v1/contact/": {
      "post": {
        "operationId": "sendContactMessage",
        "tags": [
          "Contact"
        ],
        "summary": "Send Matthews Wong a message",
        "description": "Emails the message to Matthews Wong, with the sender as reply-to. Limited to 5 requests per minute per client. Send only real messages from a person; automated or bulk submissions are not welcome.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The message was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactSuccess"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamError"
          }
        }
      }
    },
    "/api/chat/": {
      "post": {
        "operationId": "askAssistantUnversioned",
        "tags": [
          "Assistant"
        ],
        "summary": "Ask the assistant about Matthews Wong (deprecated, use /api/v1/chat/)",
        "description": "Deprecated alias of POST /api/v1/chat/, with the same request and responses. Every response carries a `Deprecation` header and a `Link` to the successor.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChatRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The answer, as a stream of server-sent events.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Link": {
                "$ref": "#/components/headers/DeprecationLink"
              }
            },
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                },
                "example": "data: {\"delta\":\"Matthews is an AI Forward\"}\n\ndata: {\"delta\":\" Deployed Engineer...\"}\n\ndata: {\"done\":true}\n\n"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "deprecated": true
      }
    },
    "/api/chat/followups/": {
      "post": {
        "operationId": "suggestFollowupsUnversioned",
        "tags": [
          "Assistant"
        ],
        "summary": "Suggest follow-up questions for an answer (deprecated, use /api/v1/chat/followups/)",
        "description": "Deprecated alias of POST /api/v1/chat/followups/, with the same request and responses. Every response carries a `Deprecation` header and a `Link` to the successor.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FollowupsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Zero to three suggested questions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FollowupsResponse"
                }
              }
            },
            "headers": {
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Link": {
                "$ref": "#/components/headers/DeprecationLink"
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          }
        },
        "deprecated": true
      }
    },
    "/api/contact/": {
      "post": {
        "operationId": "sendContactMessageUnversioned",
        "tags": [
          "Contact"
        ],
        "summary": "Send Matthews Wong a message (deprecated, use /api/v1/contact/)",
        "description": "Deprecated alias of POST /api/v1/contact/, with the same request and responses. Every response carries a `Deprecation` header and a `Link` to the successor.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The message was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactSuccess"
                }
              }
            },
            "headers": {
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Link": {
                "$ref": "#/components/headers/DeprecationLink"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamError"
          }
        },
        "deprecated": true
      }
    }
  },
  "components": {
    "schemas": {
      "ChatRequest": {
        "type": "object",
        "required": [
          "message"
        ],
        "properties": {
          "message": {
            "type": "string",
            "minLength": 1,
            "maxLength": 1000,
            "description": "The question."
          },
          "language": {
            "type": "string",
            "enum": [
              "english",
              "indonesian"
            ],
            "default": "english",
            "description": "Language of the answer."
          },
          "intent": {
            "type": "string",
            "enum": [
              "chat",
              "explain"
            ],
            "default": "chat",
            "description": "`explain` asks for a 2-4 sentence explanation of `message` as a selected passage instead of a conversational answer."
          },
          "userName": {
            "type": "string",
            "maxLength": 100,
            "description": "The visitor's name, used only to address them."
          },
          "currentRoute": {
            "type": "string",
            "description": "Path of the page the question was asked on, e.g. `/en/projects/`; narrows the context."
          },
          "conversationHistory": {
            "type": "array",
            "description": "Earlier turns, oldest first. Only the last 6 are used.",
            "items": {
              "$ref": "#/components/schemas/ChatHistoryTurn"
            }
          }
        }
      },
      "ChatHistoryTurn": {
        "type": "object",
        "required": [
          "type",
          "content"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "user",
              "bot"
            ]
          },
          "content": {
            "type": "string"
          }
        }
      },
      "FollowupsRequest": {
        "type": "object",
        "required": [
          "lastBotReply"
        ],
        "properties": {
          "lastBotReply": {
            "type": "string",
            "minLength": 1,
            "maxLength": 4000,
            "description": "The assistant's last answer."
          },
          "language": {
            "type": "string",
            "enum": [
              "english",
              "indonesian"
            ],
            "default": "english"
          }
        }
      },
      "FollowupsResponse": {
        "type": "object",
        "required": [
          "followups"
        ],
        "properties": {
          "followups": {
            "type": "array",
            "maxItems": 3,
            "items": {
              "type": "string",
              "maxLength": 80
            }
          }
        }
      },
      "ContactRequest": {
        "type": "object",
        "required": [
          "name",
          "email",
          "message"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 2,
            "maxLength": 100
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 200
          },
          "message": {
            "type": "string",
            "minLength": 10,
            "maxLength": 5000
          },
          "intent": {
            "type": "string",
            "enum": [
              "hr",
              "project"
            ],
            "default": "project",
            "description": "`hr` for hiring enquiries, `project` for work enquiries."
          },
          "company": {
            "type": "string",
            "maxLength": 120,
            "description": "With `intent: hr`."
          },
          "role": {
            "type": "string",
            "maxLength": 120,
            "description": "With `intent: hr`."
          },
          "projectType": {
            "type": "string",
            "maxLength": 80,
            "description": "With `intent: project`."
          },
          "timeline": {
            "type": "string",
            "maxLength": 80,
            "description": "With `intent: project`."
          }
        }
      },
      "ContactSuccess": {
        "type": "object",
        "required": [
          "success"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          }
        }
      },
      "ApiIndex": {
        "type": "object",
        "required": [
          "name",
          "version",
          "openapi",
          "documentation",
          "endpoints"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "version": {
            "type": "string",
            "description": "The current API version, as in the path."
          },
          "openapi": {
            "type": "string",
            "format": "uri"
          },
          "documentation": {
            "type": "string",
            "format": "uri",
            "description": "The developer guide."
          },
          "endpoints": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "method",
                "path",
                "summary"
              ],
              "properties": {
                "method": {
                  "type": "string"
                },
                "path": {
                  "type": "string"
                },
                "summary": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "success",
          "error",
          "code",
          "hint",
          "docs"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string",
            "description": "Human-readable message, safe to show to a person."
          },
          "code": {
            "type": "string",
            "enum": [
              "invalid_json",
              "invalid_request",
              "rejected_input",
              "rate_limited",
              "not_found",
              "method_not_allowed",
              "service_unavailable",
              "upstream_error",
              "internal_error"
            ],
            "description": "Stable machine-readable error code."
          },
          "hint": {
            "type": "string",
            "description": "How to recover."
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "description": "This API description."
          },
          "retryAfter": {
            "type": "integer",
            "minimum": 0,
            "description": "Seconds until a retry is allowed (rate limits only)."
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The body is not JSON (`invalid_json`), fails validation (`invalid_request`) or was refused (`rejected_input`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "Only POST is accepted (`method_not_allowed`).",
        "headers": {
          "Allow": {
            "$ref": "#/components/headers/Allow"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests from this client (`rate_limited`).",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ServerError": {
        "description": "The server cannot handle the request (`service_unavailable`, `internal_error`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "UpstreamError": {
        "description": "The email provider refused the message (`upstream_error`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "headers": {
      "Deprecation": {
        "description": "When this endpoint was deprecated, as an RFC 9745 date (`@` and Unix seconds).",
        "schema": {
          "type": "string",
          "pattern": "^@-?[0-9]+$"
        }
      },
      "DeprecationLink": {
        "description": "RFC 8288 links: `rel=\"successor-version\"` to the endpoint that replaces this one, `rel=\"deprecation\"` to the policy (https://www.matthewswong.com/en/developers/#versioning).",
        "schema": {
          "type": "string"
        }
      },
      "Allow": {
        "description": "The methods this endpoint accepts.",
        "schema": {
          "type": "string"
        }
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "RateLimitLimit": {
        "description": "Requests allowed per minute per client.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitRemaining": {
        "description": "Requests left in the current window.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitReset": {
        "description": "Seconds until the window resets.",
        "schema": {
          "type": "integer"
        }
      }
    }
  }
}