Quickstart

Ship the first OTP flow from your Next.js backend.

This page is now the first real help surface for LoginRelay, not just a document index. The narrow live path is: verify one domain, issue one API key, send one OTP, verify it once, and inspect the outcome in the workspace.

Open ConsoleReview pricingProof host: https://loginrelay.com

1. Prepare

Verify a sending domain and issue an API key

Start in /dashboard/domains and /dashboard/api-keys. The public API is server-to-server only, and the domains surface now acts like a real registry instead of a single setup card.

2. Send

Create one challenge

POST /v1/send-otp returns 202 with a stable challengeId and expiry window.

3. Verify

Claim the OTP exactly once

POST /v1/verify-otp now uses a D1 compare-and-swap success claim so one challenge does not mint duplicate success responses.

Prerequisites

What must already exist

  • One verified sending domain in the workspace.
  • One active API key from the dashboard.
  • Available credits unless the workspace is on the `custom` tier.
  • Manual DNS remains the universal path, and `/dashboard/domains` now shows a searchable domain registry with a selected-domain detail pane plus the live SES records, copy-ready DNS controls, the latest persisted SES refresh result, a public DNS evidence snapshot, and a recent domain-activity timeline after each verification or DNS step.
  • Your app must call LoginRelay from the server, not directly from the browser.
LOGINRELAY_API_BASE_URL=https://loginrelay.com
LOGINRELAY_API_KEY=lr_test_xxxxx_secret
LOGINRELAY_FROM_ADDRESS=auth@project-a.com
curl -X POST https://loginrelay.com/v1/send-otp \
  -H "Authorization: Bearer lr_test_xxxxx_secret" \
  -H "Content-Type: application/json" \
  --data '{
    "from": "auth@project-a.com",
    "email": "builder@example.com",
    "purpose": "login",
    "metadata": {
      "appUserId": "user_123"
    }
  }'

# 202 Accepted
{
  "ok": true,
  "challengeId": "chl_123",
  "expiresAt": "2026-03-21T12:00:00Z"
}

Send OTP

Current live behavior

  • The domain must belong to the current workspace and already be verified.
  • Wallet debit happens before the request is accepted for non-`custom` plans.
  • Disposable-email blocking and rolling-window send limits are live now.
  • Accepted sends persist durable challenge and event rows before the queue worker advances them.

Verify OTP

What the verify path guarantees today

  • The first winning check returns a signed assertion token.
  • Concurrent losing checks now return `ERR_OTP_ALREADY_USED` instead of minting duplicate success responses.
  • Replay attempts stay visible in audit history and can advance the challenge timeline to `consumed`.
  • The assertion is still an integration artifact, not yet a public JWKS contract.
curl -X POST https://loginrelay.com/v1/verify-otp \
  -H "Authorization: Bearer lr_test_xxxxx_secret" \
  -H "Content-Type: application/json" \
  --data '{
    "email": "builder@example.com",
    "code": "123456",
    "challengeId": "chl_123",
    "purpose": "login"
  }'

# 200 OK
{
  "ok": true,
  "verified": true,
  "challengeId": "chl_123",
  "verifiedAt": "2026-03-21T12:02:00Z",
  "assertion": {
    "token": "signed_assertion_here",
    "expiresAt": "2026-03-21T12:07:00Z"
  }
}
const baseUrl =
  process.env.LOGINRELAY_API_BASE_URL ?? "https://loginrelay.com";

export async function sendOtp(email: string) {
  const response = await fetch(`${baseUrl}/v1/send-otp`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.LOGINRELAY_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      from: process.env.LOGINRELAY_FROM_ADDRESS,
      email,
      purpose: "login",
    }),
    cache: "no-store",
  });

  return response.json();
}

export async function verifyOtp(
  email: string,
  code: string,
  challengeId: string,
) {
  const response = await fetch(`${baseUrl}/v1/verify-otp`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.LOGINRELAY_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      email,
      code,
      challengeId,
      purpose: "login",
    }),
    cache: "no-store",
  });

  return response.json();
}

Next.js Example

Use LoginRelay from server routes or server actions

  • Keep the API key on the server only.
  • Use the returned `challengeId` as your next-step verify reference.
  • Persist your own application session only after a successful verify response.
  • Point the base URL at the proof host today so later domain swaps stay parameter-driven.

Common Failures

What to expect during the first integration

  • `ERR_DOMAIN_NOT_VERIFIED`: finish DNS and verification refresh first.
  • `ERR_CREDITS_EXHAUSTED`: add credits or move the workspace to the owner-managed custom tier.
  • `ERR_TEMP_EMAIL_BLOCKED`: the recipient domain is on the disposable-email block list.
  • `ERR_RATE_LIMITED`: the workspace crossed the current rolling-window send threshold.
  • `ERR_DOMAIN_TEST_CAP_REACHED`: the verified domain reached the active launch-envelope send or recipient cap once that switch is enabled.
  • `ERR_OTP_ALREADY_USED`: the challenge was already claimed successfully or replayed afterward.

Runtime Truth

What is live today versus still pending

  • Dashboard and admin login use the shared auth center only.
  • OTP send and verify are live behind dashboard-issued API keys.
  • Domain onboarding now supports both manual DNS and Cloudflare-connected DNS automation for live SES record writes, and the domains console now preflights Cloudflare zone lookup plus DNS record listing before it stores a token.
  • The domains console now persists public DNS evidence during live refresh, so each domain can show which records already match public resolvers and which still need propagation.
  • The domains console now shows per-domain launch-envelope usage and remaining allowance inside a searchable registry and selected-domain workbench before `ERR_DOMAIN_TEST_CAP_REACHED` would ever surprise the operator.
  • The current outbound provider path is still the temporary `log` worker while Amazon SES parameters are pending.
  • Commercial top-ups still start from the owner-assisted request queue in `/dashboard/wallet`, not a public checkout flow.
  • Owner review can now write a workspace-visible follow-up message and next-step link back into the wallet queue.
  • The workspace can now confirm payment or send a reply back from the same wallet queue before the owner releases credits.
  • The launch test-envelope caps for Starter and Growth are documented already, but the proof host still keeps that enforcement switch disabled until packaging is frozen.
  • Public support intake stays on support@loginrelay.com.

Commercial top-up path

Owner-assisted credits now stay in the product workflow

Before self-serve checkout exists, the live commercial path is still auditable: the wallet opens the request, owner admin sends the next step, the workspace posts settlement confirmation back on the same request, and credits land only after review.

Step 1

Submit a wallet request

The workspace opens `/dashboard/wallet` and records a top-up, plan-review, or cap-review request instead of relying on off-platform chat alone.

Step 2

Receive owner instructions

Owner admin moves the request into review, writes the next-step message, and can attach an invoice, support-mail handoff, or other payment action link.

Step 3

Confirm payment in-product

After paying or replying on the invoice thread, the workspace posts the transfer reference back on the same request row so support and fulfillment stay auditable.

Step 4

Release credits after settlement review

Owner admin fulfills the request with settlement evidence, credits the wallet, and leaves the final resolution message visible in the same workspace history.

Document Map

Repo docs that back up the live quickstart

The help page above is the public getting-started surface. The markdown pack below remains the technical source of truth for product, contracts, proof, and active rollout decisions.

docs/01-product-overview.md

Product Overview

ICP, wedge, promise, and stage-A success criteria.

docs/02-prd-v2.md

PRD v2

Functional requirements, anti-abuse rules, and acceptance criteria.

docs/03-mvp-scope.md

MVP Scope

First ship surface, release gates, and phased build checklist.

docs/04-architecture-pack.md

Architecture Pack

Boundaries, Cloudflare primitives, shell direction, and risks.

docs/05-api-and-data-contracts.md

API Contracts

OTP endpoints, dashboard auth model, and data drafts.

docs/13-quickstart-nextjs-server-guide.md

Next.js Quickstart

First server-side integration guide for send, verify, and common failure handling.

docs/10-project-implementation-rules.md

Implementation Rules

Auth center, minimal admin, support inbox, and skill policy.

docs/11-staging-proof.md

Staging Proof

Cloudflare proof-domain evidence, compatibility fixes, and next blockers.

The markdown source of truth still lives in the repository under docs/. The public help page now carries the first real quickstart, while the markdown pack remains the deeper technical reference.