API Docs

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 6 start or 20 verify requests per minute from one IP, or more than 6 codes per email per hour. Retry-After is 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.