The Link Hiding Inside Every Setup QR Code

Every time you scan a 2FA setup QR code with your phone, the camera is reading a plain text string in a specific format called an otpauth URI. It looks like a web address, except it starts with otpauth:// instead of https://, and it is never sent anywhere. Your authenticator app parses it locally and stores what it needs to generate codes for that one account. Once you can read this format yourself, 2FA setup stops being a black box: you can verify what a QR code actually contains, troubleshoot a code mismatch, rebuild a lost entry manually, or move an account between apps without guessing.

The Exact Format

The otpauth URI follows this structure:

otpauth://TYPE/LABEL?PARAMETERS

Where TYPE is either totp (time based, refreshes every 30 seconds) or hotp (counter based, advances one step per use). LABEL identifies the account, usually as Issuer:account. PARAMETERS is a standard URL query string carrying the secret and configuration values. Nothing about this format is exotic. It reuses ordinary URI syntax that any browser or parser already understands, which is a large part of why it caught on so widely.

A Worked Example, Parameter by Parameter

otpauth://totp/GitHub:jane@example.com?secret=JBSWY3DPEHPK3PXP&issuer=GitHub&algorithm=SHA1&digits=6&period=30

Breaking that down piece by piece:

  • otpauth:// the scheme. It tells the app "this is a one time password enrollment link," nothing more.
  • totp the type. This account uses time based codes. The other option, hotp, is covered in our TOTP vs HOTP comparison.
  • GitHub:jane@example.com the label. The part before the colon is conventionally the issuer name, the part after is the account identifier. This is what shows up in your app's list, not something used in the math.
  • secret=JBSWY3DPEHPK3PXP the shared secret, encoded in Base32 (see our Base32 explainer for why that encoding specifically). Every single code your app will ever generate for this account is derived from this one value combined with the current time.
  • issuer=GitHub a second, redundant copy of the service name. It exists as a separate parameter because early implementations only read the label, and some apps needed a structured field instead of parsing text out of a string.
  • algorithm=SHA1 the hashing function used inside the HMAC that produces each code. SHA1 is the default almost everywhere and remains cryptographically fine for this specific purpose, since TOTP does not depend on SHA1 resisting collision attacks the way, say, a certificate signature would. Some services specify SHA256 or SHA512 instead.
  • digits=6 how many digits the generated code has. Six is standard. A handful of services issue eight.
  • period=30 how many seconds each code remains valid before the next one is calculated. Thirty seconds is the near universal default.

Any parameter that is omitted falls back to these same defaults (SHA1, 6 digits, 30 second period), which is why a bare otpauth://totp/Label?secret=XXXX with nothing else still works in practice.

Why This Became a Standard Without Ever Being an Official One

Here is the detail most people never learn: the otpauth URI was never ratified by the IETF or any standards body the way TOTP itself was (RFC 6238) or HOTP was (RFC 4226). It came from Google Authenticator's original open source implementation around 2010, which needed some way to encode a QR payload and picked this URI shape as an internal convention. Because Google Authenticator was the first authenticator app nearly everyone installed, every competing app (Authy, Microsoft Authenticator, FreeOTP, and the hundreds that followed, including ours) had a strong incentive to read the exact same format so their apps would work with the same QR codes services were already generating. The format spread by gravity, not by committee. Two decades later it functions as a de facto standard purely because so much of the ecosystem depends on it, in the same way plenty of "how the internet actually works" details were never formally specified, just adopted by everyone at once.

Why the Secret Parameter Is the Entire Story

Everything else in the URI is bookkeeping. The secret parameter alone determines every code that will ever be generated for that account. This has two direct consequences:

  • A setup QR code is a password, not a picture. A screenshot of it sitting in your camera roll or a photo of it taped inside a desk drawer is your live 2FA secret in plaintext, readable by anyone who sees the image.
  • The secret is also your backup. If you record the raw Base32 string somewhere safe, you can rebuild the account in a brand new app on a brand new phone without touching account recovery at all. Our manual setup key guide walks through saving it properly.

An otpauth URI is a self contained recipe. Hand any standards compliant authenticator this one string and it will produce identical codes to the original. That portability is exactly why it needs to be protected like a credential, not treated like a QR code for a restaurant menu.

Reading and Building One Yourself

You do not need special software to see the otpauth URI behind a QR code. Feed the image into our QR decoder and the raw otpauth:// string comes back out, entirely in your browser. Going the other direction, if you already have a valid otpauth URI and want a scannable QR to enroll a second device, paste it into our QR generator. And to confirm that a secret is producing the codes you expect before you commit to it, drop it into our homepage TOTP generator and compare the output against your authenticator app.

Frequently Asked Questions

Why do some codes work in one app but not another?

Almost always a parameter mismatch. If a service issues digits=8, period=60, or a nonstandard algorithm and the receiving app quietly assumes the usual defaults instead of reading those fields, the generated codes will not match. Scanning the QR is more reliable than typing only the secret by hand for exactly this reason: the QR carries every parameter, a manually typed secret carries none of them.

Is Steam Guard's code an otpauth URI?

No. Steam uses its own proprietary five character scheme that does not follow this format at all, which is why a generic authenticator app cannot add a Steam account by scanning its QR code. Our Steam Guard guide has the specifics. Nearly every other major service, from GitHub to your bank, uses the standard otpauth format described above.

Can I paste an otpauth URI directly into a password manager?

Most password managers with built in TOTP support, including Bitwarden and 1Password, accept the QR code, the bare secret, or the complete otpauth URI in their one time password field, then parse out the parameters automatically. See our roundup of password managers with built in TOTP for specifics on each.

What does the issuer parameter actually change?

Almost nothing cryptographically. It labels the entry clearly in your app's list and helps prevent two accounts with the same account name from becoming indistinguishable. The secret alone does the mathematical work; issuer is there purely so a crowded authenticator app stays navigable.

Is it safe to generate a QR code from my own otpauth URI using an online tool?

Only if the tool runs entirely client side in your browser, which is how ours works, transmitting nothing to a server. Because the URI you would be pasting contains your live, working secret, never paste it into a server side generator: doing so hands your 2FA to whoever operates that server. If you enroll a second device this way, use a trusted local tool and delete any leftover copies of the QR image afterward.

Shoyeb Akter

Written by

Security Tools Developer and creator of 2FA Fast, a privacy-first browser-based authenticator and security tools platform.