CHIRP // DOCS

Everything, on
one page.

Read top-to-bottom, or jump via the sidebar. Ctrl+F searches the whole reference at once.

Introduction

Sign in with Chirp is standard OpenID Connect — with two differences from Google / Auth0 / Clerk: you never receive the user’s email, and the user ID you get (the sub) is unique to your app.

That’s the whole integration model. Register an app, drop in any standards-compliant OIDC client, and you get back a signed ID token whose sub is stable per user, scoped to your app, and carries no email. Everything else — how the user actually signs in, what device they use, how they recover access — is the user’s side, and Chirp handles it. You never have to reason about it to ship.

The two differences are the reason to choose Chirp.
You get a stable per-user ID scoped to your app, and you never hold their email — so you can’t leak it, sell it, or be subpoenaed for it. See How it works for the why; you don’t need it to integrate.

Getting access

Fully self-serve — no waitlist, no approval step. Sign in with a magic link (no password), open your dashboard, and register your first app. You can go from nothing to a production sign-in button in one sitting. Questions: hello@chirpauth.com.

Once enabled, sign in at /dashboard with the email address you wrote from. Everything below assumes you're in.

Quickstart

Required reading: it’s standard OIDC plus the two differences. Register an app, drop in any OIDC client, exchange the code — you get a token whose sub is per-app and which carries no email. Nothing about how the user signs in, what tier they’re on, or how they recover access enters your code. (Needs a developer account — see Getting access.)

1 · REGISTER (DASHBOARD)
/dashboard → New app
  name          some-ecommerce-site.com
  redirect_uri  https://some-ecommerce-site.com/auth/cb

→ client_id  cs_dev_7f3a09c2…
  (a public PKCE client — no secret to keep)

Prefer raw HTTP? The dashboard drives the same control plane: POST /control/apps with your ID token as Authorization: Bearer registers the identical app. Client ids are cs_dev_… or cs_live_… — the prefix tells you the environment.

2 · DISCOVER
GET https://signin.chirpauth.com/.well-known/openid-configuration

3 · Add the “Sign in with Chirp” button (see integrate: web) and exchange the returned code at /token. Done.

OIDC reference

Authorization Code flow with PKCE. All responses JSON; all tokens signed RS256.

GET /.well-known/openid-configuration
{
  "issuer": "https://signin.chirpauth.com",
  "authorization_endpoint": ".../authorize",
  "token_endpoint": ".../token",
  "jwks_uri": ".../jwks.json",
  "scopes_supported": ["openid"],
  "response_types_supported": ["code"],
  "code_challenge_methods_supported": ["S256"]
}
POST /token
grant_type=authorization_code
&code=<one-time>&redirect_uri=https://…/auth/cb
&client_id=cs_dev_7f3a…&code_verifier=<pkce>

→ { "id_token":"<jwt>", "access_token":"…",
    "token_type":"Bearer", "expires_in":3600 }

Integrate: web

Any standards-compliant OIDC client works. Node + Express with openid-client:

NODE · EXPRESS · OPENID-CLIENT
const chirp  = await Issuer.discover('https://signin.chirpauth.com');
const client = new chirp.Client({
  client_id: process.env.CHIRP_ID,
  redirect_uris: ['https://…/auth/cb'],
  response_types: ['code'],
  token_endpoint_auth_method: 'none', // public PKCE client
});
app.get('/login', (req, res) => res.redirect(
  client.authorizationUrl({ scope: 'openid',
    code_challenge_method: 'S256' })));

Drop in the official button — copy-paste HTML lives on /brand. Don't restyle the mark.

Integrate: iOS

Use ASWebAuthenticationSession with a claimed HTTPS callback (Universal Link). Never embed a WebView — Apple requires the system browser for OAuth, and so do we.

SWIFTUI
let session = ASWebAuthenticationSession(
  url: authorizeURL, callbackURLScheme: nil
) { callback, error in
  guard let code = callback?.queryItem("code") else { return }
  exchangeForToken(code)   // POST /token + PKCE
}
session.prefersEphemeralWebBrowserSession = false
session.start()

Integrate: Android

Chrome Custom Tabs with an App Link callback. AppAuth handles PKCE and the token exchange.

KOTLIN · APPAUTH
val req = AuthorizationRequest.Builder(
  config, CLIENT_ID, ResponseTypeValues.CODE,
  Uri.parse("https://…/auth/cb"))
  .setScope("openid").build()   // PKCE auto-added
service.performAuthorizationRequest(req, ok, cancel)

Declare the callback host with android:autoVerify="true" so Android opens it without a chooser.

ADVANCED · SKIP TO SHIP

How it works

You can skip this whole section and still integrate. It’s reference for the curious: how the per-app sub is derived, how users actually sign in, and the upgrade path on the user’s side. None of it appears in your code — the token is the same no matter which of these a user is on.

Per-app (pairwise) identity

Most identity providers hand every app the same stable id, so any two apps can compare lists and discover they share a user. Chirp never does that — the sub we give an app is derived, not stored:

HOW SUB IS COMPUTED
sub = HMAC(server_key, user_id || client_id)
ONE USER, TWO APPS
user_id = usr_9f3a   (never leaves Chirp)

some-ecommerce-site.com (cli_AAAA) → sub = 3b91c2e7…ad
notes.hello.io          (cli_BBBB) → sub = f07d4419…2c

compare subs → no match. Can't tell it's one user.

The mapping is one-way: an app can’t reverse a sub back to the user’s id — and apps never receive an email address at all. There is no email scope; Chirp keeps the address server-side. The only scope is openid. (Internally a user can hold multiple personas off one root_sub; your app still just sees one stable per-app sub — the persona machinery never crosses the token boundary.)

How users sign in

Users land on signin.chirpauth.com — the same page every Chirp app shares — and sign in one of two ways. You never see or choose which:

  • Email link. A short-lived link to the address they signed up with. Nothing to remember, nothing to type.
  • Passkey. A key pair bound to a device and a domain — the private half never leaves the device; Chirp stores only the public half. Signing in is a Face ID / Touch ID / PIN prompt, nothing to phish. Supported by iCloud Keychain, Google Password Manager, Windows Hello, and hardware keys (YubiKey).
Chirp Zero & Chirp One (the user’s upgrade path)

Chirp Zero is the easy way in — an email-link account, no password, no setup. Chirp One is the user’s real passkey-secured account, reached as a contextual upgrade later, with recovery codes as a backup way in if they lose every passkey. This is purely the end user’s story: a developer never reasons about which tier a user is on. The ID token is identical either way — same shape, same per-app sub, no email. Don’t branch on it.

Troubleshooting

Every error page carries a reference id (e.g. 7Q3K-2M11-XZ22) and, for developers, a trace id. Include the reference id when you email support — it's the only handle we have, because we don't store who you are.

Error
Likely cause & fix
redirect_uri_mismatch
URI in /authorize isn't registered. Add it on the app's detail page under /dashboard.
auth_req_expired
User took >10 min. Restart the flow; nothing was signed in.
link_expired
Magic link older than 10 min. Links are reusable within the window, so this means the link expired, not that it was used. Send a fresh one.
invalid_client
Unknown client_id, or wrong secret on a confidential app. Check it against the app's detail page.