The otpauth URI Behind Every 2FA QR Code, and How to Build One
What the otpauth:// URI is, how its issuer, account and secret parameters work, and how to turn it into a QR code your authenticator app can scan to enroll.
The otpauth URI Behind Every 2FA QR Code, and How to Build One
When you set up two-factor authentication and a site shows you a square QR code to scan, that image is not magic. It is a tiny text string painted into a grid of black and white squares. Your authenticator app reads the string with its camera and starts producing six-digit codes. The string has a name and a strict format: it is an otpauth:// URI, and once you can read one, the whole 2FA setup flow stops feeling like a black box.
This post breaks down what that URI carries, what each parameter means, and how to go from a bare secret to a scannable QR code yourself. If you want to build or parse one as you read, the otpauth URI generator does both in the browser.
What the otpauth URI actually is
An otpauth:// URI is a small piece of text that an authenticator app understands. It encodes everything the app needs to compute one-time codes, so the app never has to phone home to the service that issued it. Here is the shape:
otpauth://totp/Issuer:account?secret=BASE32&issuer=Issuer&algorithm=SHA1&digits=6&period=30
That single line tells the app: this is a time-based token (totp), it belongs to a provider called Issuer, the account is account, the shared key is BASE32, and codes should be six digits computed with SHA1 on a thirty-second clock. The app reads those fields, stores them, and from then on shows a live code that rotates every thirty seconds. Almost every "scan this QR" 2FA setup hides exactly this string inside the image.
The scheme breaks into a few clear pieces:
- Type (
totporhotp) sits right after theotpauth://prefix. Time-based (totp) is what every mainstream service uses; counter-based (hotp) shows up only in a handful of legacy hardware tokens. - Label is the path:
Issuer:account. The part before the colon is a prefix that repeats the issuer; the part after is the account identifier, usually your email or username. - Query parameters carry the rest:
secret,issuer,algorithm,digits, andperiod.
The parameters, one at a time
The secret is the heart of it. It is the shared key that both the server and your app use to derive codes, written in Base32 (RFC 4648), which uses only the letters A to Z and the digits 2 to 7. That alphabet deliberately drops 0, 1, 8 and 9 so nobody confuses them with O, I, B and g. A typical secret is 16 or 32 characters. Everything else in the URI is metadata; the secret is the one value that must stay private.
The issuer is the provider name, and it does double duty. It appears both in the label prefix and as a standalone issuer= parameter, and your authenticator uses it as the display name in its list. Skip it and three different work accounts all show up as the same bare email, leaving you unable to tell which code belongs to which login.
The account is what distinguishes entries under the same issuer, typically alice@example.com. Because labels and issuers often contain spaces, @ signs and colons, they have to be URL-encoded so the URI does not fall apart. A well-built generator handles that encoding for you.
The last three are small but unforgiving. algorithm is almost always SHA1; most authenticator apps only fully support it, so picking SHA256 to feel safer makes every code wrong. digits is 6 by convention, occasionally 8. period is 30 seconds. Treat all three as fixed unless the service documentation explicitly tells you otherwise, because a mismatch here produces codes that look valid and are rejected every single time.
A worked example
Say GitHub is enrolling an account for alice@example.com and hands you the Base32 secret JBSWY3DPEHPK3PXP. The full URI is:
otpauth://totp/GitHub:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=GitHub&algorithm=SHA1&digits=6&period=30
Read it left to right: time-based token, issuer GitHub, account alice@example.com, secret JBSWY3DPEHPK3PXP, standard SHA1 with six digits on a thirty-second cycle. Drop that string into any authenticator and the entry appears as "GitHub (alice@example.com)" producing a fresh code every half minute. Change period to 60 or algorithm to SHA256 and the math still runs, but the numbers no longer line up with what GitHub's server computes, which is the most common reason a brand-new token "never works."
Turning the URI into a scannable QR code
Authenticator apps add accounts with the camera, not by pasting text. So once you have the URI, the last step is to render it as a QR code and scan that image. The flow is short: assemble the URI, copy it, paste it into a QR code generator, and point your authenticator at the result. A few password managers, such as 1Password and Bitwarden, also accept the raw otpauth:// string in a setup field, which skips the QR step entirely.
This is genuinely useful when a service shows you only a secret and no QR code, or when its QR image refuses to scan. You take the bare key, wrap it in a correct URI, generate your own QR, and enroll the account without any back-and-forth.
A note on local generation and safety
I once had to migrate a dozen accounts off an authenticator that flatly refused to export them. I still had every original secret saved in a password vault, so I rebuilt each otpauth:// URI by hand, generated a QR for each, and scanned them into the new app. Every code lined up on the first try because the secret and parameters were identical, and I never had to disable and re-enable 2FA on any of the sites. What made that comfortable was knowing the work was happening on my own machine.
That matters because a 2FA secret is as sensitive as a password. Generation should be local. In this generator, building the URI, parsing one back, and creating a random Base32 secret are all plain JavaScript running in your browser tab. The secret is never sent over the network, never logged, and never written into the shareable URL; only the non-secret options round-trip in a shared link. Even so, for demos, screenshots or testing, reach for the random-secret button and generate a throwaway value rather than pasting a real key from your bank or work login.
Putting it together
The otpauth:// URI is the quiet standard underneath every 2FA QR code. Once you can read otpauth://totp/Issuer:account?secret=...&issuer=..., you can build setup links by hand, migrate accounts between apps, generate disposable test tokens, and debug the parameter mismatches that cause rejected codes. Build the URI in the otpauth URI generator, render it with a QR tool, and you control the whole enrollment yourself, secret and all.
Made by Toolora · Updated 2026-06-13