The Morrow libraryReading 2 of 7
A note on api reference

A conversation with the API

Two small exchanges: asking to hear more, and choosing to leave. This is their exact request and response behavior.

Browse the library Download the OpenAPI contract ↓
The public website contract

There are two endpoints. Neither performs a financial operation.

Keep a copy of the OpenAPI 3.1 contract ↓
POST /api/early-access

First, an invitation to stay in touch.

A registration asks the service to store an email with explicit consent to receive early-access updates. The request contains these fields.

email
Required string, trimmed and lowercased. Maximum 254 characters, with an ASCII local part of at most 64 characters. Leading, trailing or consecutive local-part dots are rejected. The domain must contain a dot; internationalized domains use punycode.
consent
Required. JSON accepts true or "on". A URL-encoded form sends on. Other values are rejected.
website
An optional honeypot. Omit it or leave it empty. Non-empty trimmed strings are rejected.

Extra fields are ignored. Syntax validation does not prove the address exists or belongs to the sender.

// Run in the website, with the visitor's form fields.
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 || 'Signup is not confirmed.');
}
// New registrations return a private removalToken.
// Save it: a duplicate does not return it again.
Joining through the website

Then, a reply you can rely on.

A success response follows a successful D1 operation. Until that response arrives, the browser must treat the result as unconfirmed.

  1. 201 · A new JSON signup

    The email was persisted. The response contains success: true and removalToken, a random 64-character lowercase hexadecimal credential.

  2. 200 · An existing JSON signup

    The response is {"success":true}. The stored row and original removal token are retained. That credential is not returned again.

  3. 200 · A native form reply

    A successful URL-encoded form receives an HTML confirmation. New registrations also see their private removal link and token. No email is sent by the endpoint.

POST /api/remove

And a way to leave.

Send a required token string matching ^[a-f0-9]{64}$. The service hashes it and deletes the matching signup. It does not need an email address.

// removalToken comes from the visitor's private link.
const response = await fetch('/api/remove', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ token: removalToken })
});
const result = await response.json();
if (!response.ok || result.success !== true) {
  throw new Error(result.error || 'Removal is not confirmed.');
}
Removing a registration

A successful JSON request receives 200 with {"success":true}. A form request receives an HTML confirmation. A valid token with no matching record also succeeds, so repeating removal is safe and does not reveal whether a signup existed.

This endpoint shares the request checks below. It does not use the signup rate limiter.

The shared ground rules.

Paths are relative to this website’s origin and are unversioned. Both endpoints accept only POST, with JSON or URL-encoded form bodies capped at 2,048 bytes.

JSON requests receive JSON. Successful URL-encoded requests receive HTML; errors are always JSON. The Accept header does not choose the response format.

Responses use Cache-Control: no-store. Browser requests must be same-origin. There is no API-key mechanism or cross-origin CORS access. An absent Origin header is allowed for non-browser clients.

Making space for everyone.

After validation, signup allows ten attempts per network address in each server Unix-time hourly bucket. Duplicates count. The next attempt receives 429 and Retry-After in seconds. The counter uses an atomic database operation.

When a request cannot complete.

Errors contain success: false and a human-readable error string. There is no additional machine-readable error-code field.

400Invalid input, malformed JSON, missing consent, a filled honeypot or an invalid removal token.

Check the request against the endpoint contract before resubmitting.

403A supplied Origin differs from the request origin, or Sec-Fetch-Site is cross-site.

Check the request against the endpoint contract before resubmitting.

405The request method is not POST. The response includes Allow: POST.

Check the request against the endpoint contract before resubmitting.

413The request body exceeds 2,048 bytes.

Check the request against the endpoint contract before resubmitting.

415Content-Type is not application/json or application/x-www-form-urlencoded.

Check the request against the endpoint contract before resubmitting.

429Signup attempt limit reached. Retry-After contains the seconds until the next hourly bucket. Signup only.

Wait for the Retry-After interval before trying again.

503The required database binding is unavailable or the database operation failed.

The service could not confirm completion. Keep the interface in an unconfirmed state and offer a later retry.

For a network failure or 503, do not display a success state. Repeated signup submissions do not create extra email records.