Latch 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: $LATCH_API_KEY" https://auth.amgcc.space/api/codes
{
"codes": [
{
"id": 1,
"issuer": "GitHub",
"label": "[email protected]",
"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: $LATCH_API_KEY" https://auth.amgcc.space/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: latch_your_key_here
https://auth.amgcc.space — HTTPS only.latch_, so they are recognisable in a
log or a secret scanner.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: $LATCH_API_KEY" https://auth.amgcc.space/api/codes
Live code for one account — so a job only ever sees the single code it needs.
curl -H "X-API-Key: $LATCH_API_KEY" https://auth.amgcc.space/api/codes/1
{
"id": 1,
"issuer": "GitHub",
"label": "[email protected]",
"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: $LATCH_API_KEY" https://auth.amgcc.space/api/accounts
{
"accounts": [
{"id": 1, "issuer": "GitHub", "label": "[email protected]",
"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://auth.amgcc.space/api/accounts \
-H "X-API-Key: $LATCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"secret":"JBSWY3DPEHPK3PXP","issuer":"GitHub","label":"[email protected]"}'
# same thing, straight from the otpauth:// URI the setup QR encodes
curl -X POST https://auth.amgcc.space/api/accounts \
-H "X-API-Key: $LATCH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"uri":"otpauth://totp/GitHub:[email protected]?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: $LATCH_API_KEY" https://auth.amgcc.space/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. |
403.period
seconds, so polling faster gains nothing.import os, requests
r = requests.get("https://auth.amgcc.space/api/codes",
headers={"X-API-Key": os.environ["LATCH_API_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://auth.amgcc.space/api/codes/1", {
headers: { "X-API-Key": process.env.LATCH_API_KEY },
});
if (!res.ok) throw new Error(`latch ${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.LATCH_API_KEY }}" \
https://auth.amgcc.space/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 a Latch code is the same code your phone's authenticator would show.