Terndays
← Back to Terndays

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.

EndpointPurpose
GET /.well-known/oauth-authorization-serverRFC 8414 discovery metadata — issuer, authorize/token/register/revoke URLs
POST /oauth/registerRFC 7591 dynamic client registration
GET /oauth/authorizeConsent screen; signs you in first if needed
POST /oauth/tokenExchanges a code (or refresh token) for an access token
POST /oauth/revokeRFC 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

ScopeGrantable?Notes
counters.readYes — the default if scope is omittedPer-counter state: the totals and days remaining the app already shows you
ledger.readNo — permanentlyYour 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.