Latch

API reference

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.

Quickstart Authentication Reading codes Managing accounts Errors Limits Language examples Security
Your API key

Examples below use the placeholder $LATCH_API_KEY. A free account mints a real key instantly.

Create a free account

Quickstart

Three steps, about a minute.

curl -H "X-API-Key: $LATCH_API_KEY" https://latch.cc/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: $LATCH_API_KEY" https://latch.cc/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

Authentication

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

Reading codes

GET/api/codes

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://latch.cc/api/codes

GET/api/codes/<id>

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://latch.cc/api/codes/1
{
  "id": 1,
  "issuer": "GitHub",
  "label": "ci-bot@example.com",
  "code": "130604",
  "seconds_remaining": 22,
  "digits": 6,
  "period": 30,
  "algorithm": "SHA1"
}

Managing accounts

GET/api/accounts

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://latch.cc/api/accounts
{
  "accounts": [
    {"id": 1, "issuer": "GitHub", "label": "ci-bot@example.com",
     "digits": 6, "period": 30, "algorithm": "SHA1"}
  ]
}

POST/api/accounts

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.

FieldTypeNotes
secretstringBase32; spaces are fine. Required unless uri is given.
uristringotpauth://totp/… — fills in every other field for you.
issuerstringService name, e.g. GitHub. Optional.
labelstringWhich account at that service. Optional.
digitsint6–8. Default 6.
periodintSeconds per code. Default 30.
algorithmstringSHA1 (default), SHA256, SHA512.
curl -X POST https://latch.cc/api/accounts \
  -H "X-API-Key: $LATCH_API_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://latch.cc/api/accounts \
  -H "X-API-Key: $LATCH_API_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.

DELETE/api/accounts/<id>

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://latch.cc/api/accounts/1

Returns {"ok": true}, or 404 if that id is not yours.

Errors

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"}
StatusMeaning
200Fine.
201Account created.
400Body missing, or the secret is not valid base32 / not a TOTP otpauth:// URI.
401Missing, malformed or unknown X-API-Key.
403Free-plan cap reached (50 entries).
404No such id in your vault — including ids that exist but belong to somebody else.
405Wrong method for that path; the body lists what is allowed.

Limits

Language examples

Python

import os, requests

r = requests.get("https://latch.cc/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")

Node

const res = await fetch("https://latch.cc/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);

GitHub Actions

- name: Fetch a 2FA code
  id: totp
  run: |
    code=$(curl -sf -H "X-API-Key: ${{ secrets.LATCH_API_KEY }}" \
      https://latch.cc/api/codes/1 | jq -r .code)
    echo "::add-mask::$code"
    echo "code=$code" >> "$GITHUB_OUTPUT"

Security, honestly

An API that returns 2FA codes gives up the “something you have” factor. Whoever holds your Latch key holds the second factor for every account in that vault. That is the whole point for automation, and exactly why the key deserves the same care as a password.

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.