Two requests. Two jobs.
Register an email. Remove a registration. The complete HTTP contract of the early-access website.
Before making a request.
Both endpoints accept application/json and application/x-www-form-urlencoded. JSON submissions receive JSON. Successful URL-encoded submissions receive an HTML confirmation; their errors remain JSON. The Accept header does not select the response format.
Browser requests must be same-origin. A supplied Origin must match this website, and cross-site Sec-Fetch-Site is rejected. An absent Origin is allowed for non-browser clients. CORS access is not enabled. Responses use Cache-Control: no-store.
/api/early-access
Send an email and consent.
Input is validated before the signup rate counter is incremented.
- email REQUIRED STRING
- Trimmed and lowercased. At most 254 characters; local part at most 64. No leading, trailing or consecutive dots in the local part. ASCII mailbox syntax; use punycode for internationalized domains. Domain must contain a dot.
- consent REQUIRED
- JSON accepts
trueor"on". An HTML form sendson. Other values are rejected. - website OPTIONAL HONEYPOT
- Omit or leave empty. Non-empty trimmed string values are rejected.
Additional fields are ignored. Syntax validation does not confirm mailbox ownership or delivery.
// Use the values from your own same-origin form.
const response = await fetch('/api/early-access', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: emailInput.value,
consent: consentInput.checked,
website: ''
})
});
const result = await response.json();
if (!response.ok || result.success !== true) {
throw new Error(result.error || 'Not confirmed.');
}
// Save result.removalToken privately if present.
// Duplicates do not return a new token.# Local database. Use an email address you control.
: "${SIGNUP_EMAIL:?Set SIGNUP_EMAIL first}"
curl -i http://localhost:8791/api/early-access \
-H 'Origin: http://localhost:8791' \
--data-urlencode "email=$SIGNUP_EMAIL" \
--data-urlencode 'consent=on'Read the confirmed result.
New JSON signup. A row was saved. The response contains success: true and removalToken: a random 64-character lowercase hexadecimal credential. Save it privately.
Existing JSON signup. Returns {"success":true}. The original removal credential is retained and never disclosed by an email lookup.
Successful native form. Returns an HTML confirmation. A newly saved signup also shows its private removal link and token.
A success response requires a successful D1 operation. This endpoint does not send email. Validated attempts, including duplicates, share a limit of ten per network address per Unix-time hourly bucket. The next attempt receives 429 and Retry-After seconds.
/api/remove
Present the private credential.
- token REQUIRED STRING
- Must match
^[a-f0-9]{64}$. The server hashes it and deletes the matching signup. No email address is needed.
JSON success is {"success":true} with status 200. Native form success is an HTML confirmation.
An unknown but well-formed token also succeeds. Removal is idempotent and does not reveal whether a row existed. It uses the shared request checks but not the signup rate limiter.
// token comes from the saved private removal link.
const response = await fetch('/api/remove', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token })
});
const result = await response.json();
if (!response.ok || result.success !== true) {
throw new Error('Removal was not confirmed.');
}The website puts it after # in the private link, then submits it in a POST body. Do not put it in query parameters or logs.
When a request fails.
Errors are JSON objects with success: false and a human-readable error string. There is no separate machine-readable error code.
- 400
- Invalid input, malformed JSON, missing consent, a filled honeypot or an invalid removal token.
- 403
- A supplied Origin differs from the request origin, or Sec-Fetch-Site is cross-site.
- 405
- The request method is not POST. The response includes Allow: POST.
- 413
- The request body exceeds 2,048 bytes.
- 415
- Content-Type is not application/json or application/x-www-form-urlencoded.
- 429
- Signup attempt limit reached. Retry-After contains the seconds until the next hourly bucket. Signup only.
- 503
- The required database binding is unavailable or the database operation failed.
Correct invalid input before retrying. Respect Retry-After. For network failures and 503, show an unconfirmed state and offer a later retry.
A row may be saved even if its response is lost. A retry then returns duplicate success without the original removal token. There is no email delivery or credential-recovery flow in this implementation.