{
  "openapi": "3.1.0",
  "info": {
    "title": "Geekie API",
    "version": "1.0.0",
    "summary": "Public endpoints of geekie.nl, the website of Geekie Creative & IT.",
    "description": "Geekie Creative & IT builds websites, software and SEO for small businesses in the Rotterdam area. This API exposes the free website check and the Markdown versions of every page for agents. Every error is JSON with `code`, `message`, `hint` and `docs`.\n\nHuman-readable docs: https://geekie.nl/developers. Overview for language models: https://geekie.nl/llms.txt.",
    "contact": {
      "name": "Geekie Creative & IT",
      "url": "https://geekie.nl/contact",
      "email": "info@geekie.nl"
    },
    "license": {
      "name": "All rights reserved, Geekie Creative & IT"
    }
  },
  "externalDocs": {
    "description": "Geekie API documentation",
    "url": "https://geekie.nl/developers"
  },
  "servers": [
    {
      "url": "https://geekie.nl"
    }
  ],
  "tags": [
    {
      "name": "Website check",
      "description": "The free website check from https://geekie.nl/blog/gratis-website-check."
    },
    {
      "name": "Content",
      "description": "Every page of geekie.nl, also as Markdown for agents."
    },
    {
      "name": "Contact",
      "description": "The contact form of geekie.nl."
    }
  ],
  "paths": {
    "/api": {
      "get": {
        "tags": [
          "Content"
        ],
        "operationId": "getApiIndex",
        "summary": "List the API endpoints",
        "responses": {
          "200": {
            "description": "Endpoint overview",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "405": {
            "description": "`method_not_allowed`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/website-check": {
      "post": {
        "tags": [
          "Website check"
        ],
        "operationId": "runWebsiteCheck",
        "summary": "Run a mobile Lighthouse audit of a public website",
        "description": "Asks Google PageSpeed Insights for a mobile Lighthouse audit and returns the four category scores (0-100). A run can take up to a minute. Rate limit: 4 checks per 10 minutes and 12 per day per IP address.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "maxLength": 300,
                    "description": "A public website address, with or without https://.",
                    "examples": [
                      "example.nl"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Audit result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "scores",
                    "finalUrl"
                  ],
                  "properties": {
                    "scores": {
                      "type": "object",
                      "properties": {
                        "performance": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "minimum": 0,
                          "maximum": 100
                        },
                        "accessibility": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "minimum": 0,
                          "maximum": 100
                        },
                        "bestPractices": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "minimum": 0,
                          "maximum": 100
                        },
                        "seo": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "minimum": 0,
                          "maximum": 100
                        }
                      }
                    },
                    "finalUrl": {
                      "type": "string",
                      "format": "uri"
                    },
                    "lcp": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Largest Contentful Paint as displayed by Lighthouse, e.g. \"2.1 s\"."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_url`: the url is missing or not a public http(s) website",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "`method_not_allowed`: use POST",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: too many checks from this address; see Retry-After",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`audit_failed`: PageSpeed Insights could not audit the site",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/md/{path}": {
      "get": {
        "tags": [
          "Content"
        ],
        "operationId": "getPageMarkdown",
        "summary": "Get any page as Markdown",
        "description": "Returns the readable content of a page as Markdown: `/md/diensten` is https://geekie.nl/diensten. The same Markdown is served at the page's own URL when the request sends `Accept: text/markdown`.",
        "parameters": [
          {
            "name": "path",
            "in": "path",
            "required": true,
            "description": "The page path without the leading slash, e.g. `diensten/seo-en-geo`. Use `/md` for the homepage.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The page as Markdown",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "No page at this path; a Markdown body links to llms.txt and the sitemap",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/contact/token": {
      "get": {
        "tags": [
          "Contact"
        ],
        "operationId": "getContactToken",
        "summary": "Issue a token for the contact form",
        "description": "Used by the contact forms on geekie.nl before they send a message.",
        "responses": {
          "200": {
            "description": "A fresh token",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/contact": {
      "post": {
        "tags": [
          "Contact"
        ],
        "operationId": "sendContactMessage",
        "summary": "Send a message through the contact form",
        "description": "Only accepted from the forms on geekie.nl (same-origin browser requests with a form token). Agents that want to reach Geekie should point their user to https://geekie.nl/contact or email info@geekie.nl instead.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "email",
                  "message"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "company": {
                    "type": "string",
                    "maxLength": 160
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 254
                  },
                  "phone": {
                    "type": "string",
                    "maxLength": 40
                  },
                  "subject": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "message": {
                    "type": "string",
                    "maxLength": 5000
                  },
                  "locale": {
                    "type": "string",
                    "enum": [
                      "nl",
                      "en"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_body`, `missing_fields`, `invalid_email`, `field_too_long`, `invalid_order`, `invalid_email`/`invalid_phone`/`invalid_verification` (with `field`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "`method_not_allowed`: use POST",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`not_configured`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`send_failed`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error",
          "code",
          "message",
          "docs"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Legacy error value read by the site's own forms."
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable error code.",
            "examples": [
              "invalid_url"
            ]
          },
          "message": {
            "type": "string",
            "description": "What went wrong, in plain English."
          },
          "hint": {
            "type": "string",
            "description": "How to fix the request."
          },
          "docs": {
            "type": "string",
            "format": "uri"
          },
          "field": {
            "type": "string",
            "description": "The form field the error is about, when there is one."
          }
        }
      }
    }
  },
  "security": []
}