AutoAuthAPI stores your two-factor accounts and hands the live TOTP codes back over HTTP. One header, five endpoints, JSON in and JSON out. Every example on this page runs as-is once your key is in place.
Three steps, about a minute.
curl -H "X-API-Key: $AUTOAUTHAPI_KEY" https://autoauthapi.com/api/codes
{
"codes": [
{
"id": 1,
"issuer": "GitHub",
"label": "ci-bot@example.com",
"code": "130604",
"seconds_remaining": 22,
"digits": 6,
"period": 30,
"algorithm": "SHA1"
}
]
}
seconds_remaining is how long this code stays valid. When it is
low, wait the window out instead of submitting a code that expires mid-request:
#!/bin/sh # print a code with a full window ahead of it while :; do json=$(curl -sf -H "X-API-Key: $AUTOAUTHAPI_KEY" https://autoauthapi.com/api/codes/1) left=$(printf '%s' "$json" | jq -r .seconds_remaining) [ "$left" -ge 5 ] && printf '%s\n' "$json" | jq -r .code && break sleep "$left" done
Send your key in the X-API-Key header on every request. No OAuth
dance, no token exchange, no expiry.
X-API-Key: autoauth_your_key_here
https://autoauthapi.com — HTTPS only.autoauth_, so they are recognisable in a
log or a secret scanner. Older keys may start with
latch_; they keep working unchanged until you
rotate them.401.404, identical to
an id that does not exist.Live code for every account in your vault. An empty vault returns an empty list, not an error.
curl -H "X-API-Key: $AUTOAUTHAPI_KEY" https://autoauthapi.com/api/codes
Live code for one account — so a job only ever sees the single code it needs.
curl -H "X-API-Key: $AUTOAUTHAPI_KEY" https://autoauthapi.com/api/codes/1
{
"id": 1,
"issuer": "GitHub",
"label": "ci-bot@example.com",
"code": "130604",
"seconds_remaining": 22,
"digits": 6,
"period": 30,
"algorithm": "SHA1"
}
Your accounts, metadata only — no codes, no secrets. This is how
you discover the id values the other endpoints take.
curl -H "X-API-Key: $AUTOAUTHAPI_KEY" https://autoauthapi.com/api/accounts
{
"accounts": [
{"id": 1, "issuer": "GitHub", "label": "ci-bot@example.com",
"digits": 6, "period": 30, "algorithm": "SHA1"}
]
}
Store a new 2FA secret. Send either the raw base32
secret a site reveals under “can't scan the QR code?”,
or the whole otpauth:// URI that the QR encodes.
| Field | Type | Notes |
|---|---|---|
secret | string | Base32; spaces are fine. Required unless uri is given. |
uri | string | otpauth://totp/… — fills in every other field for you. |
issuer | string | Service name, e.g. GitHub. Optional, up to 200 characters. A number is accepted and stored as its text; true/false, null, lists and objects are not. |
label | string | Which account at that service. Optional, up to 200 characters. A number is accepted and stored as its text (4711 becomes "4711"). |
digits | int | 6, 7 or 8. Default 6. A whole-number string such as "6" or "6.0" is accepted too. |
period | int | Seconds per code, 1–300. Default 30. A whole-number string such as "30" or "30.0" is accepted too. |
algorithm | string | SHA1 (default), SHA256, SHA512. Case-insensitive. |
curl -X POST https://autoauthapi.com/api/accounts \
-H "X-API-Key: $AUTOAUTHAPI_KEY" \
-H "Content-Type: application/json" \
-d '{"secret":"JBSWY3DPEHPK3PXP","issuer":"GitHub","label":"ci-bot@example.com"}'
# same thing, straight from the otpauth:// URI the setup QR encodes
curl -X POST https://autoauthapi.com/api/accounts \
-H "X-API-Key: $AUTOAUTHAPI_KEY" \
-H "Content-Type: application/json" \
-d '{"uri":"otpauth://totp/GitHub:ci-bot@example.com?secret=JBSWY3DPEHPK3PXP&issuer=GitHub"}'
201 with the
created account. Metadata only — the secret is never echoed back.
Permanently remove an account and its stored secret. Make sure you have another way into that service first.
curl -X DELETE -H "X-API-Key: $AUTOAUTHAPI_KEY" https://autoauthapi.com/api/accounts/1
Returns
{"ok": true}, or 404 if that id is not yours.
Every error is JSON with a single error string at the matching
HTTP status. Nothing under /api/ ever returns an HTML page.
{"error": "invalid or missing API key"}
| Status | Meaning |
|---|---|
200 | Fine. |
201 | Account created. |
400 | Body missing or not a JSON object, a field outside the limits above, or the secret is not valid base32 / not a TOTP otpauth:// URI. |
401 | Missing, malformed or unknown X-API-Key. |
403 | Free-plan cap reached (50 entries). |
404 | No such id in your vault — including ids that exist but belong to somebody else. |
405 | Wrong method for that path; the body lists what is allowed. |
413 | Request body larger than 64 KB. |
429 | Too many requests: your account's per-minute allowance, or too many failed
authentications from your address (see Limits). The body adds
retry_after, and a Retry-After header gives the same number of seconds. |
403.429:
HTTP/1.1 429 Too Many Requests
Retry-After: 12
{"error": "rate limit exceeded", "retry_after": 12}
retry_after seconds and send it again; refused requests are
not counted, so a client that waits is let straight back in. Codes only
change every period seconds, so polling faster than once a
second gains nothing anyway.X-API-Key from one address in a minute, further requests from
it with a missing or wrong key get 429 instead of
401 until the minute has passed. Requests with a valid key are
never affected, from that address or any other.import os, requests
r = requests.get("https://autoauthapi.com/api/codes",
headers={"X-API-Key": os.environ["AUTOAUTHAPI_KEY"]},
timeout=15)
r.raise_for_status()
for c in r.json()["codes"]:
print(c["issuer"], c["code"], c["seconds_remaining"], "s left")
const res = await fetch("https://autoauthapi.com/api/codes/1", {
headers: { "X-API-Key": process.env.AUTOAUTHAPI_KEY },
});
if (!res.ok) throw new Error(`AutoAuthAPI ${res.status}: ${await res.text()}`);
const { code, seconds_remaining } = await res.json();
if (seconds_remaining < 5) await new Promise(r => setTimeout(r, seconds_remaining * 1000));
console.log(code);
- name: Fetch a 2FA code
id: totp
run: |
code=$(curl -sf -H "X-API-Key: ${{ secrets.AUTOAUTHAPI_KEY }}" \
https://autoauthapi.com/api/codes/1 | jq -r .code)
echo "::add-mask::$code"
echo "code=$code" >> "$GITHUB_OUTPUT"
/api/, so a
leaked key cannot be used to walk off with your seeds — which is what keeps
rotating that key a real remedy rather than a partial one.otpauth:// URI are the
same secret three ways, with nothing encrypting any of them. Treat it as a
bearer credential: whoever obtains it generates valid codes for that
account indefinitely, without your password, and it cannot be revoked
short of re-enrolling two-factor at the site itself. That is why the path is
in the browser, takes your password on the request that returns the secret,
and does one entry at a time — a session cookie alone will not do it, and an
API key cannot reach it at all. (Why it works this way:
the reasoning is on the privacy page.)Codes are generated by a TOTP implementation written against RFC 6238 using only the Python standard library and checked against the RFC's own test vectors, so an AutoAuthAPI code is the same code your phone's authenticator would show.