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 an email link or a passkey, or choose Create an account with a passkey. Save your account ID and recovery codes, then register your first app in the dashboard. Questions: hello@chirpauth.com.

No operator setup is needed. The dashboard is where you register apps, edit callback URLs, and permanently retire apps when you are finished.

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 a Chirp CLI control access 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

This complete local example uses Rust and openidconnect 4.0.1. Install Rust and Cargo first. In the dashboard, register http://127.0.0.1:4178/auth/cb as your app’s redirect URI.

Download these public files into a new directory:

mkdir -p chirp-web/src chirp-web/.cargo
cd chirp-web
curl -fsS https://signin.chirpauth.com/quickstart/Cargo.toml -o Cargo.toml
curl -fsS https://signin.chirpauth.com/quickstart/Cargo.lock -o Cargo.lock
curl -fsS https://signin.chirpauth.com/quickstart/src/main.rs -o src/main.rs
curl -fsS https://signin.chirpauth.com/quickstart/.env.example -o .env
curl -fsS https://signin.chirpauth.com/quickstart/.gitignore -o .gitignore
curl -fsS https://signin.chirpauth.com/quickstart/.cargo/audit.toml -o .cargo/audit.toml

Edit .env: replace CHIRP_CLIENT_ID with the client ID from your dashboard. Then run:

cargo run --locked

Open http://127.0.0.1:4178 and choose Sign in with Chirp. The callback displays OIDC verified and your app-specific sub. Refreshing that callback must fail; start a new login from the app’s home page.

The example generates and stores state, nonce, and a PKCE verifier in a one-use transaction bound to an HttpOnly browser cookie. The SDK validates the signed ID token and checks that UserInfo returns the same subject. Ordinary access tokens authorize only UserInfo; they cannot call Chirp control APIs. This example does not retain tokens or create a persistent app session.

For production, use HTTPS and Secure cookies, shared transaction storage for multiple instances, and your own app session after validation. See the complete instructions and limits. Any standard OIDC SDK can implement this same flow.

Finish and clean up

Open the app in your dashboard, type its client ID, and choose Permanently retire app. This cannot be undone and prevents future sign-ins and token exchanges; sessions your app already created follow your app’s own expiry policy. If recent authentication is required, sign out from your Chirp account page and sign in again.

After retiring all owned apps and cancelling any subscription, you can delete your account from account management. Download your current Chirp account data from account export.

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. Use sub as your app’s login identifier. Tokens never expose the account root, so separate personas cannot be linked through a root claim.

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.