{
  "openapi": "3.1.0",
  "info": {
    "title": "Watchtower Waitlist API",
    "version": "1.0.0",
    "summary": "Join the Watchtower early access waitlist.",
    "description": "Watchtower by NorthQA is a QA platform for Playwright and Cypress teams, in early access. This API has one job: adding an email address to the waitlist so its owner is invited when a seat opens up. It is free, needs no sign-up and no API key. Test against it safely with the sandbox mode: add `?dry_run=true` and the request is validated but not recorded. Only submit an email address with its owner's consent.\n\n**Errors** use RFC 9457 problem details (`application/problem+json`) with a stable `code` and a `hint` describing how to fix the request.\n\n**Rate limits:** 5 requests per IP address per 60-second window. Every `/api/*` response, including errors and 404s, carries the IETF RateLimit headers (`RateLimit-Policy`, `RateLimit`, plus the earlier `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`); a 429 also carries `Retry-After`.\n\n**Versioning and deprecation** (full policy: https://watchtower.systems/developers#versioning): the major version is in the URL path (`/api/v1`). Changes within v1 are backward compatible: new optional fields or new endpoints only. Breaking changes ship under a new path (`/api/v2`). A version scheduled for removal returns the `Deprecation` header (RFC 9745) and a `Sunset` header (RFC 8594) with the removal date at least 90 days in advance, plus a `Link` header with `rel=\"successor-version\"`.",
    "contact": {
      "name": "NorthQA",
      "email": "hello@northqa.com",
      "url": "https://watchtower.systems/contact"
    }
  },
  "servers": [
    {
      "url": "https://watchtower.systems/api/v1",
      "description": "Production, API version 1"
    }
  ],
  "security": [],
  "paths": {
    "/waitlist": {
      "post": {
        "operationId": "joinWaitlist",
        "summary": "Join the waitlist",
        "description": "Adds an email address to the Watchtower early access waitlist. NorthQA emails that address when a seat opens up. Submitting the same address again is harmless. Add ?dry_run=true to test the call without recording anything.",
        "tags": [
          "Waitlist"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WaitlistRequest"
              },
              "example": {
                "email": "you@company.com",
                "source": "my-agent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The email address is on the waitlist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WaitlistResponse"
                },
                "example": {
                  "ok": true,
                  "message": "You're on the list. We'll email you when your Watchtower seat opens up."
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            }
          },
          "400": {
            "description": "The body isn't valid JSON.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "The method isn't POST.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "Allow": {
                "$ref": "#/components/headers/Allow"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The body is over 2048 bytes.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "415": {
            "description": "The Content-Type isn't application/json.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "The email, source or dry_run value is invalid.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests from this IP address in the current window.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "502": {
            "description": "The waitlist service didn't confirm the signup; retry later.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "dry_run",
            "in": "query",
            "required": false,
            "description": "Sandbox mode. When true, the request is fully validated and rate limited but nothing is recorded, and the response includes \"dry_run\": true. Use it to test an integration. Values other than true or false are rejected with invalid_dry_run.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "WaitlistRequest": {
        "type": "object",
        "required": [
          "email"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254,
            "description": "The address to invite when a seat opens up."
          },
          "source": {
            "type": "string",
            "pattern": "^[A-Za-z0-9._-]{1,64}$",
            "default": "api",
            "description": "Optional short identifier for where the signup came from, such as the agent's name."
          }
        }
      },
      "WaitlistResponse": {
        "type": "object",
        "required": [
          "ok",
          "message"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "message": {
            "type": "string",
            "description": "A confirmation to show the user."
          },
          "dry_run": {
            "type": "boolean",
            "const": true,
            "description": "Present and true when the request ran in sandbox mode and nothing was recorded."
          }
        }
      },
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details with Watchtower's code and hint extension members.",
        "required": [
          "type",
          "title",
          "status",
          "detail",
          "code",
          "hint"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "description": "Link to this schema."
          },
          "title": {
            "type": "string",
            "description": "Short summary of the problem."
          },
          "status": {
            "type": "integer",
            "description": "The HTTP status code."
          },
          "detail": {
            "type": "string",
            "description": "What went wrong with this request."
          },
          "code": {
            "type": "string",
            "enum": [
              "invalid_json",
              "method_not_allowed",
              "payload_too_large",
              "unsupported_media_type",
              "invalid_email",
              "invalid_source",
              "invalid_dry_run",
              "rate_limited",
              "upstream_unavailable",
              "not_found"
            ],
            "description": "Stable, machine-readable error code."
          },
          "hint": {
            "type": "string",
            "description": "How to fix the request or what to do next."
          }
        },
        "example": {
          "type": "https://watchtower.systems/openapi.json#/components/schemas/Problem",
          "title": "Invalid email",
          "status": 422,
          "detail": "The email field is missing or isn't a valid email address.",
          "code": "invalid_email",
          "hint": "Provide the email field as a string like you@company.com, at most 254 characters."
        }
      }
    },
    "headers": {
      "RateLimit-Policy": {
        "description": "The quota policy: q requests per w seconds (draft-ietf-httpapi-ratelimit-headers).",
        "schema": {
          "type": "string",
          "example": "\"api\";q=5;w=60"
        }
      },
      "RateLimit": {
        "description": "Remaining requests (r) and seconds until the window resets (t) (draft-ietf-httpapi-ratelimit-headers).",
        "schema": {
          "type": "string",
          "example": "\"api\";r=4;t=42"
        }
      },
      "RateLimit-Limit": {
        "description": "Requests allowed per window.",
        "schema": {
          "type": "integer",
          "example": 5
        }
      },
      "RateLimit-Remaining": {
        "description": "Requests left in the current window.",
        "schema": {
          "type": "integer",
          "example": 4
        }
      },
      "RateLimit-Reset": {
        "description": "Seconds until the current window resets.",
        "schema": {
          "type": "integer",
          "example": 42
        }
      },
      "Retry-After": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "integer",
          "example": 42
        }
      },
      "Allow": {
        "description": "Methods this endpoint accepts.",
        "schema": {
          "type": "string",
          "example": "POST"
        }
      }
    }
  },
  "externalDocs": {
    "description": "Developer guide: quickstart, errors, rate limits, versioning and deprecation policy.",
    "url": "https://watchtower.systems/developers"
  }
}
