{"ns":"otp","title":"Passcode","version":"1.0","summary":"Passcode is a passwordless sign-in service. A person enters an email address, receives a 6-digit one-time code in a built-in demo mailbox, types it into six one-digit boxes and lands on an account page. It also offers sign-up, resend, expiry, attempt limits and a small HTTP API.","problem":"Passwords are forgotten and reused. People want to sign in with only an email address, and the service must make sure that only the person who controls the mailbox, holding the newest unexpired code, can get in.","personas":[{"name":"Returning member","need":"Sign in with just an email address and a short code, quickly and without a password."},{"name":"New visitor","need":"Create an account with a name and email and be signed in with the first code."},{"name":"Account holder under attack","need":"Be protected from guessed, replayed or stale codes."},{"name":"Integrator","need":"Do the same flow over HTTP with a bearer token as the result."}],"goals":["Only the newest, unexpired code for an address can sign a person in, and only within three tries.","Every failure explains itself in one clear sentence and tells the person what to do next.","The code shown in the mailbox is exactly the code the service accepts.","The flow works with keyboard, paste and reload without losing the person in the middle."],"features":[{"id":"SGN","title":"Sign-up","summary":"Self-service account creation with a name and an inbox address.","reqs":[{"id":"FR-SGN-01","weight":"Must","text":"Creating an account with a name and an unused address sends a code and signing in with it opens the account page.","accept":["\"Create account\" moves to a \"Check your inbox\" screen saying a 6-digit code was sent to a masked address such as a•••@inbox.test","After the sixth digit is typed the code verifies automatically and the page shows \"Welcome back, <name>\" with Plan \"Free\""]},{"id":"FR-SGN-02","weight":"Should","text":"Sign-up requires a name and an unused address on the demo mail domain.","accept":["An empty form shows \"Enter your name\" and \"Enter your email\"","An address outside the demo domain shows \"Use an @inbox.test address so you can read the code\"","An address that already has an account shows \"An account with this email already exists — sign in instead\""]}]},{"id":"REQ","title":"Requesting a code","summary":"The sign-in screen asks for an email address and sends a code to existing accounts only.","reqs":[{"id":"FR-REQ-01","weight":"Must","text":"An existing member who enters their email address is sent a code and, after entering it, sees their account details.","accept":["The screen says \"We sent a 6-digit code to d•••@inbox.test\" for dev.patel@inbox.test","After the code the page shows \"Welcome back, Dev Patel\", Plan \"Free\", Member since 2026-01-09 and a first recent sign-in marked \"— this session\"","If a code was requested for the same address in the last 30 seconds the message is \"Please wait N s\" instead"]},{"id":"FR-REQ-02","weight":"Should","text":"An address without an account is rejected with a link to sign up.","accept":["The message is \"No account found for this email · Create an account\" and the person stays on the sign-in screen"]},{"id":"FR-REQ-03","weight":"Should","text":"Empty and malformed addresses are rejected before any code is sent.","accept":["An empty field shows \"Enter your email\"","\"mira.okafor@inbox\" shows \"Enter a valid email address\""]},{"id":"FR-REQ-04","weight":"Should","text":"A suspended account cannot request a code and receives no code mail.","accept":["The message is \"This account is suspended. Contact support.\"","The mailbox for that address holds only \"Your account has been suspended\" and no sign-in code"]}]},{"id":"MBX","title":"Demo mailbox","summary":"A built-in mailbox page, opened in a second tab, stands in for real email.","reqs":[{"id":"FR-MBX-01","weight":"Must","text":"Each code email shows the 6-digit code and its expiry, and that code is accepted when entered exactly as shown.","accept":["The list shows a masked subject \"•••••• is your Passcode sign-in code\"; the full code is visible only inside the message","The message body shows a 6-digit code and its expiry time","Entering the code exactly as displayed signs the person in; it is never rejected as incorrect"]},{"id":"FR-MBX-02","weight":"Could","text":"The mailbox lists the messages for one address, newest first.","accept":["mira.okafor@inbox.test shows at least \"New sign-in from Firefox on Linux\" and \"Welcome to Passcode\", newer above older","The welcome message body starts \"Hi Mira Okafor,\""]}]},{"id":"VER","title":"Verifying a code","summary":"Entering the code, with a limited number of tries and a limited lifetime.","reqs":[{"id":"FR-VER-01","weight":"Should","text":"A wrong code is rejected, the boxes clear, and the remaining attempts count down.","accept":["First wrong code: \"Incorrect code. 2 attempts left.\"","Second wrong code: \"Incorrect code. 1 attempt left.\""]},{"id":"FR-VER-02","weight":"Must","text":"Three wrong codes lock the current code: even the correct code is refused until a new code is requested.","accept":["The third wrong code shows \"Too many attempts. Request a new code.\" and the boxes are disabled","After a resend the message is \"A new code is on its way. Earlier codes no longer work.\", the boxes are enabled and the newest code signs the person in","No number of further tries on the locked code can succeed"]},{"id":"FR-VER-03","weight":"Should","text":"A code expires after its lifetime and is then refused.","accept":["The default lifetime is 2 minutes; the verify screen shows a live \"Code expires in m:ss\" and then \"Code expired\"","A shorter lifetime can be set with an optional ttl URL parameter (minimum 5 seconds); with 10 seconds the mail says \"It expires in 10 seconds\"","An expired code shows \"This code has expired. Request a new code.\""]}]},{"id":"RSD","title":"Resend","summary":"Asking for another code, rate-limited and replacing earlier codes.","reqs":[{"id":"FR-RSD-01","weight":"Should","text":"Resend is disabled for 30 seconds after a code is sent, with a live countdown.","accept":["The button reads \"Resend code in 30 s\", is disabled and counts down every second","When the countdown ends it reads \"Resend code\" and is enabled"]},{"id":"FR-RSD-02","weight":"Must","text":"A resent code replaces every earlier code: only the newest works.","accept":["After a resend the mailbox holds two code mails, newest on top","Entering the older code shows \"This code is no longer valid. Request a new code.\"","Entering the newest code signs the person in"]}]},{"id":"BOX","title":"Code boxes","summary":"Six one-digit boxes that behave like a single input.","reqs":[{"id":"FR-BOX-01","weight":"Could","text":"Pasting a 6-digit code into the first box fills every box and submits.","accept":["All six boxes fill and the account page opens without further clicks"]},{"id":"FR-BOX-02","weight":"Could","text":"Each box accepts one digit, focus advances, non-digits are ignored and Backspace moves back.","accept":["Typing \"12\" fills box 1 and box 2 and focuses box 3","Typing a letter enters nothing","Backspace clears the previous box and puts focus there"]}]},{"id":"SES","title":"Session and routing","summary":"A signed-in session persists across reloads and ends on sign-out.","reqs":[{"id":"FR-SES-01","weight":"Must","text":"A signed-in tab stays signed in after reload; signing out ends the session and protects the account page.","accept":["Reloading the account page still shows \"Welcome back, <name>\"","\"Sign out\" returns to the sign-in screen","Opening the account page directly afterwards redirects to the sign-in screen"]},{"id":"FR-SES-02","weight":"Could","text":"Opening the verify screen with no pending code returns to sign-in.","accept":["The address becomes the sign-in page and the \"Sign in\" heading is shown"]}]},{"id":"API","title":"HTTP API","summary":"The same flow over HTTP, ending in a bearer token.","reqs":[{"id":"FR-API-01","weight":"Should","text":"The API supports sign-up, request, mailbox read, verify and account, with the same cooldown and code rules as the website.","accept":["Sign-up answers 201; a first code request answers 200 with sentTo, ttl 120 and resendIn 30","A second request straight away answers 429 \"Please wait N s…\" with retryAfter","Verify with the mailbox code answers 200 with a token and user","The account endpoint answers 200 with a bearer token and 401 \"Sign in first\" without it"]}]}],"nfr":[{"id":"NFR-01","weight":"Must","text":"Codes are single-purpose: 6 digits, valid for one address, never reusable once superseded, expired or locked."},{"id":"NFR-02","weight":"Should","text":"Addresses are masked on screen (first letter then dots) so the screen does not reveal the full address."}],"glossary":[{"term":"Code","meaning":"A 6-digit one-time number emailed to the address; valid for 2 minutes by default."},{"term":"Superseded code","meaning":"A code replaced by a newer one after a resend."}],"outOfScope":["Real email delivery","SMS codes","Password sign-in"]}