Email code sign-in
Website session sign-in without a password. The same two calls sign in existing accounts and create new ones, so responses never reveal whether an account exists. Used by the site-wide sign-in modal (views/shared/_auth-modal.ejs, wwwroot/js/auth-modal.js).
These are website endpoints, not public API key endpoints. They require the CSRF token (X-CSRF-Token header matching the crateyard_csrf cookie). No token usage.
POST /api/auth/email-code/start
Sends a 6-digit code by email.
Request body:
| Field | Type | Notes |
|---|---|---|
| email | string | Required. Normalised to lower case. |
| turnstileToken | string | Required when Turnstile is configured (TURNSTILE_SITEKEY). |
curl -X POST 'https://crateyard.com/api/auth/email-code/start' \
-H 'Content-Type: application/json' \
-H 'X-CSRF-Token: {{csrfToken}}' \
-b 'crateyard_csrf={{csrfToken}}' \
-d '{"email":"[email protected]","turnstileToken":"{{turnstileToken}}"}'Response 200:
{"ok":true,"resendAfterSeconds":30}A second request within 30 seconds returns 200 with the remaining wait and does not send another email; the earlier code still works.
POST /api/auth/email-code/verify
Checks the code, signs in, and sets the HttpOnly session cookie. New accounts are created with a generated username, a verified email, and a default project.
Request body:
| Field | Type | Notes |
|---|---|---|
| email | string | Required. Same address as start. |
| code | string | Required. 6 digits. |
curl -X POST 'https://crateyard.com/api/auth/email-code/verify' \
-H 'Content-Type: application/json' \
-H 'X-CSRF-Token: {{csrfToken}}' \
-b 'crateyard_csrf={{csrfToken}}' \
-d '{"email":"[email protected]","code":"123456"}'Response 200 (with Set-Cookie: crateyard_session=...):
{"ok":true,"isNew":true}Code rules
- Codes expire after 10 minutes and are single use.
- Only the latest unused code for an email is accepted. After 5 wrong attempts it is locked; request a new code.
- Codes are stored as SHA-256 hashes, never in plain text.
- Failed sends are not retried automatically; the user chooses to resend.
Errors and rate limits
403: invalid CSRF token, or the account is banned.422: invalid email, failed human check, wrong or expired code, or a+alias email for a new account.429: more than 6startor 20verifyrequests per minute from one IP, or more than 6 codes per email per hour.Retry-Afteris set.503: the email could not be sent. No code is left active.
Conversion measurement
The email-code modal sends GA4 sign_up for isNew: true, otherwise login, with method: email_code.
Successful Google OAuth callbacks use the database insert result to distinguish a new account from an existing account, email link, or concurrent upsert. They set a five-minute crateyard_auth_event marker alongside the separate HttpOnly session cookie. The destination page clears the marker and sends sign_up or login with method: google once. Failed callbacks do not set a marker. No email, Google ID, session token or account ID is included in these events.
Mark sign_up and begin_checkout as GA4 key events without a default monetary value. login is an activity event, not a new-customer conversion. Analytics blocked by the browser will not be recorded.