{
  "openapi": "3.1.0",
  "info": {
    "title": "Morrow website API",
    "version": "1.0.0",
    "description": "Existing early-access website endpoints. No financial, partner, reward or trading API is implemented. Examples should be run locally with an email address you control. There are no API keys; signup is public and removal requires its private token."
  },
  "servers": [
    {
      "url": "http://localhost:8790",
      "description": "Local Pages runtime with local D1"
    },
    {
      "url": "https://getmorrow.pages.dev",
      "description": "Live website; submissions persist in production"
    }
  ],
  "paths": {
    "/api/early-access": {
      "post": {
        "operationId": "joinEarlyAccess",
        "summary": "Register for early-access updates",
        "description": "Same-origin browser form service. Maximum body 2,048 bytes. Validated signup attempts, including duplicates, share a ten-attempt hourly network bucket. An absent Origin header is allowed. A conflicting Origin or cross-site Sec-Fetch-Site is rejected. A successful duplicate does not replace or disclose the original removal token.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "consent"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "maxLength": 254,
                    "description": "Trimmed and lowercased. ASCII mailbox syntax; local part at most 64 characters, without leading, trailing or consecutive dots. Domain must contain a dot. IDN domains must use punycode."
                  },
                  "consent": {
                    "oneOf": [
                      {
                        "type": "boolean",
                        "const": true
                      },
                      {
                        "type": "string",
                        "const": "on"
                      }
                    ]
                  },
                  "website": {
                    "type": "string",
                    "description": "Optional honeypot. Omit or leave empty; non-empty trimmed text is rejected."
                  }
                }
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "consent"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "maxLength": 254,
                    "description": "Trimmed and lowercased. ASCII mailbox syntax; local part at most 64 characters, without leading, trailing or consecutive dots. Domain must contain a dot. IDN domains must use punycode."
                  },
                  "consent": {
                    "type": "string",
                    "const": "on"
                  },
                  "website": {
                    "type": "string",
                    "description": "Leave empty."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON: email already registered. HTML form: a confirmed new or existing registration; a new signup also displays its removal token.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "additionalProperties": false
                }
              },
              "text/html": {
                "schema": {
                  "type": "string",
                  "description": "Confirmation page."
                }
              }
            }
          },
          "201": {
            "description": "New JSON signup persisted. Save the removal token.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "removalToken"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "removalToken": {
                      "type": "string",
                      "pattern": "^[a-f0-9]{64}$",
                      "description": "New signup only. Save this credential; it is not returned again."
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "Invalid input, malformed JSON, missing consent, a filled honeypot or an invalid removal token.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "403": {
            "description": "A supplied Origin differs from the request origin, or Sec-Fetch-Site is cross-site.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "405": {
            "description": "The request method is not POST. The response includes Allow: POST.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            },
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string",
                  "const": "POST"
                }
              }
            }
          },
          "413": {
            "description": "The request body exceeds 2,048 bytes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "415": {
            "description": "Content-Type is not application/json or application/x-www-form-urlencoded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "429": {
            "description": "Signup attempt limit reached. Retry-After contains the seconds until the next hourly bucket. Signup only.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "pattern": "^[0-9]+$"
                },
                "description": "Seconds remaining in the current hourly bucket."
              }
            }
          },
          "503": {
            "description": "The required database binding is unavailable or the database operation failed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          }
        }
      }
    },
    "/api/remove": {
      "post": {
        "operationId": "removeEarlyAccess",
        "summary": "Remove a signup using its private token",
        "description": "Idempotent deletion by the SHA-256 hash of a 64-character lowercase hexadecimal credential. An unknown but well-formed token also receives success. Never put the token in query parameters or logs. Same request origin and body-size checks as signup. This endpoint does not use the signup rate limiter.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string",
                    "pattern": "^[a-f0-9]{64}$"
                  }
                }
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string",
                    "pattern": "^[a-f0-9]{64}$"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Deletion completed, or no matching signup existed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "additionalProperties": false
                }
              },
              "text/html": {
                "schema": {
                  "type": "string",
                  "description": "Removal confirmation page."
                }
              }
            }
          },
          "400": {
            "description": "Invalid input, malformed JSON, missing consent, a filled honeypot or an invalid removal token.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "403": {
            "description": "A supplied Origin differs from the request origin, or Sec-Fetch-Site is cross-site.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "405": {
            "description": "The request method is not POST. The response includes Allow: POST.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            },
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string",
                  "const": "POST"
                }
              }
            }
          },
          "413": {
            "description": "The request body exceeds 2,048 bytes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "415": {
            "description": "Content-Type is not application/json or application/x-www-form-urlencoded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          },
          "503": {
            "description": "The required database binding is unavailable or the database operation failed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "error"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": false
                    },
                    "error": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                }
              }
            }
          }
        }
      }
    }
  }
}