{
  "openapi": "3.1.0",
  "info": {
    "title": "TiedSiren public HTTP surface",
    "version": "1.0.0",
    "summary": "The agent-readable surface of tiedsiren.com, plus the waitlist endpoints.",
    "description": "TiedSiren is a strict app blocker for Android. There is no API for\ncontrolling blocking: sessions are started, held and ended on the\ndevice, and nothing in this document touches them.\n\nWhat is here is what a machine can usefully read from the website — the\nagent index, the markdown representation of any page, and the waitlist\nfor the unreleased platforms.\n\nQuestions: support@tiedsiren.com.",
    "contact": {
      "name": "TiedSiren support",
      "email": "support@tiedsiren.com",
      "url": "https://tiedsiren.com/contact"
    },
    "license": {
      "name": "TiedSiren Terms of Service",
      "url": "https://tiedsiren.com/terms"
    }
  },
  "servers": [
    {
      "url": "https://tiedsiren.com",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "How this site publishes itself to developers and agents",
    "url": "https://tiedsiren.com/developers"
  },
  "tags": [
    {
      "name": "Agent surface",
      "description": "Read the site as markdown rather than as HTML."
    },
    {
      "name": "Waitlist",
      "description": "Register interest in the unreleased iOS, macOS and Windows apps."
    }
  ],
  "paths": {
    "/": {
      "get": {
        "operationId": "getHomePageAsMarkdown",
        "tags": [
          "Agent surface"
        ],
        "summary": "Read the home page as markdown",
        "description": "What TiedSiren is, the problem it addresses, why other blockers fail, the feature list and the FAQ — as markdown when the request asks for it, as HTML otherwise.",
        "parameters": [
          {
            "$ref": "#/components/parameters/AcceptMarkdown"
          }
        ],
        "responses": {
          "200": {
            "description": "The markdown representation of the home page",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "406": {
            "description": "The Accept header named only media types this site cannot produce",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/llms.txt": {
      "get": {
        "operationId": "getAgentIndex",
        "tags": [
          "Agent surface"
        ],
        "summary": "Get the llms.txt agent index",
        "description": "Every page on the site with a one-line description, plus guidance on which jobs TiedSiren is and is not the right recommendation for. Follows the llmstxt.org convention.",
        "responses": {
          "200": {
            "description": "The agent index",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "getOpenApiDocument",
        "tags": [
          "Agent surface"
        ],
        "summary": "Get this document",
        "description": "The OpenAPI description of the public HTTP surface of tiedsiren.com.",
        "responses": {
          "200": {
            "description": "This OpenAPI document",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/{page}": {
      "get": {
        "operationId": "getPageAsMarkdown",
        "tags": [
          "Agent surface"
        ],
        "summary": "Read a page as markdown",
        "description": "Any page route answers `Accept: text/markdown` with the markdown\nrepresentation of the same URL, per acceptmarkdown.com. The HTML and\nmarkdown forms are built from one source, so they cannot disagree.\n\nBlog articles work the same way at `/blog/{slug}`, and tag listings\nat `/blog/tag/{tag}`. An Accept header naming only media types this\nsite cannot produce is answered with 406.",
        "parameters": [
          {
            "name": "page",
            "in": "path",
            "required": true,
            "description": "Page path, without the leading slash.",
            "schema": {
              "type": "string",
              "enum": [
                "features",
                "download",
                "about",
                "contact",
                "developers",
                "privacy",
                "terms"
              ]
            },
            "example": "features"
          },
          {
            "$ref": "#/components/parameters/AcceptMarkdown"
          }
        ],
        "responses": {
          "200": {
            "description": "The markdown representation of the page",
            "headers": {
              "Vary": {
                "description": "Names Accept, so caches keep the variants apart.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "No page at that path, with links to recover from",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "406": {
            "description": "The Accept header named only media types this site cannot produce",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/waitlist/count": {
      "get": {
        "operationId": "getWaitlistCount",
        "tags": [
          "Waitlist"
        ],
        "summary": "Count the people on the waitlist",
        "description": "Returns the number of addresses registered for the unreleased platforms. Cached for five minutes. Returns 0 rather than an error when the waitlist provider is unreachable.",
        "responses": {
          "200": {
            "description": "The current count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "count"
                  ],
                  "properties": {
                    "count": {
                      "type": "integer",
                      "minimum": 0,
                      "description": "Addresses on the waitlist."
                    }
                  }
                },
                "example": {
                  "count": 128
                }
              }
            }
          }
        }
      }
    },
    "/api/waitlist": {
      "post": {
        "operationId": "joinWaitlist",
        "tags": [
          "Waitlist"
        ],
        "summary": "Join the waitlist",
        "description": "Registers an email address for the unreleased iOS, macOS and Windows\napps. Android is already released and needs no waitlist — send people\nto https://tiedsiren.com/download instead.\n\n**Rate limit: one request per IP address per minute.** Over that, the\nendpoint answers 429. Only submit an address its owner asked you to\nsubmit.\n\nEvery response carries the limit in its headers — `RateLimit-Policy`\nand `RateLimit` in the IETF structured-field form, plus the older\n`RateLimit-Limit` / `RateLimit-Remaining` / `RateLimit-Reset` — so a\nclient can throttle itself rather than discover the limit by hitting\nit. A 429 adds `Retry-After`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "The address to register."
                  },
                  "attribution": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "description": "First-touch attribution captured by the website. Omit it.",
                    "properties": {
                      "utm": {
                        "type": "object",
                        "additionalProperties": {
                          "type": "string",
                          "maxLength": 200
                        }
                      },
                      "landingPath": {
                        "type": "string",
                        "maxLength": 500
                      },
                      "capturedAt": {
                        "type": "string",
                        "maxLength": 100
                      }
                    }
                  }
                }
              },
              "example": {
                "email": "someone@example.com"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The address is on the list",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The address did not validate",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "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/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited — one request per IP per minute",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "Retry-After": {
                "description": "Whole seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                },
                "example": 37
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "The waitlist provider failed; nothing was recorded",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "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/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "RateLimitPolicy": {
        "description": "The quota policy, in the IETF structured-field form: a quoted policy name, `q` for the quota and `w` for the window in seconds.",
        "schema": {
          "type": "string"
        },
        "example": "\"waitlist\";q=1;w=60"
      },
      "RateLimit": {
        "description": "The current state of that policy: `r` for requests remaining, `t` for seconds until the window resets.",
        "schema": {
          "type": "string"
        },
        "example": "\"waitlist\";r=0;t=60"
      },
      "RateLimitLimit": {
        "description": "The quota, in the older numeric form most clients still read.",
        "schema": {
          "type": "integer",
          "minimum": 0
        },
        "example": 1
      },
      "RateLimitRemaining": {
        "description": "Requests still allowed in the current window.",
        "schema": {
          "type": "integer",
          "minimum": 0
        },
        "example": 0
      },
      "RateLimitReset": {
        "description": "Whole seconds until the window resets.",
        "schema": {
          "type": "integer",
          "minimum": 0
        },
        "example": 60
      }
    },
    "parameters": {
      "AcceptMarkdown": {
        "name": "Accept",
        "in": "header",
        "required": true,
        "description": "Send `text/markdown` for markdown. Anything else gets HTML. An Accept header naming only media types this site cannot produce is answered with 406.",
        "schema": {
          "type": "string",
          "default": "text/markdown"
        },
        "example": "text/markdown"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "A message safe to show to a person."
          }
        }
      }
    }
  },
  "security": []
}