Authentication
Two credentials, one surface
OAuth 2.1 and self-issued API keys both work on the same endpoints, side by side — neither replaces the other. Pick whichever fits how your agent connects.
Not publicly reachable yet — the API goes live with the hosted release. Status
OAuth 2.1
This is the path claude.ai's and ChatGPT's connector pickers use: an authorization-code flow with PKCE, no client secret required for a public client. The consent screen is where you approve the connection — it names the requesting client and states plainly what it's asking for.
| Endpoint | Purpose |
|---|---|
| GET /.well-known/oauth-authorization-server | RFC 8414 discovery metadata — issuer, authorize/token/register/revoke URLs |
| POST /oauth/register | RFC 7591 dynamic client registration |
| GET /oauth/authorize | Consent screen; signs you in first if needed |
| POST /oauth/token | Exchanges a code (or refresh token) for an access token |
| POST /oauth/revoke | RFC 7009 — revokes a token |
PKCE is required on every flow, S256 only — a plain-method challenge or a missing one is refused. Authorization codes are single-use and live about 60 seconds; a replayed code revokes every token it ever produced, on the assumption that a replay means a leak.
Scopes
| Scope | Grantable? | Notes |
|---|---|---|
| counters.read | Yes — the default if scope is omitted | Per-counter state: the totals and days remaining the app already shows you |
| ledger.read | No — permanently | Your day-by-day history stays on a device you control (the desktop bridge); requesting it returns an explicit invalid_scope refusal, not a generic error |
The consent screen states this directly, in the same words a client sees before it's allowed to connect:
"Turning this on lets Terndays' server read your counter numbers to answer {Client Name}'s questions. Revoke anytime from Settings → API access." — followed by: "Terndays never reads your day-by-day travel history through this connection."
Example: exchanging a code for a token
curl -X POST https://api.terndays.app/oauth/token \
-d grant_type=authorization_code \
-d code=<code from the redirect> \
-d redirect_uri=<your redirect_uri> \
-d client_id=<your client_id> \
-d code_verifier=<your PKCE verifier>The response carries a td_oauth_at_… access token (an hour) and a td_oauth_rt_… refresh token (30 days, single-use — refreshing rotates both).
API keys
A self-issued key is the simpler path for a script, a Claude Desktop or Claude Code config, or any agent that reads a bearer credential from its own environment. You create one in the app, under Settings → API access — the plaintext key is shown once, at creation, and never stored anywhere in a form we could read back to you.
A key looks like td_live_ followed by 64 hex characters. Send it as either header:
Authorization: Bearer td_live_<your key>
# or
X-Api-Key: td_live_<your key>A key is unscoped — a valid one can call anything the hosted surface offers, the same reach as an OAuth grant with counters.read. Revoking a key, or letting Pro lapse, stops it working immediately; the key row itself stays visible so you can always see and remove an old one, even after a plan downgrade.
Which to use
Use OAuth if you're connecting through claude.ai's or ChatGPT's connector picker — that's the path they require. Use an API key for Claude Desktop, Claude Code, or a script of your own — a static bearer credential in a config file or environment variable, no browser flow needed.