Verified Ham for developers
Let your users sign in with a verified amateur radio call sign. Verified Ham is a standard OpenID Connect provider, so any OIDC or OAuth 2 client library works.
How verification works
A user creates an account, enters their call sign, and we look it up in the licensing authority's public data (the FCC in the US, ISED in Canada). We mail a postcard with a one-time code to the address on that licence; entering the code proves the user receives mail there. We re-check every verified call sign against new licence data, and revoke verification when a licence expires or a call sign is reassigned.
Who can sign in to your app
- You, always, so you can build and test before verifying.
- Everyone else once you have verified your own call sign, and only if they have verified theirs. Unverified users are shown how to verify, then sent back to your app.
New to the problem? Why typed-in call signs can't be trusted, and what to do about it.
Get started
Step-by-step guides for Discourse, WordPress, Keycloak, Next.js, Node.js, Python and plain PHP: see all guides. Try a real sign-in and see the tokens in the playground, and use the official sign-in button.
Endpoints
- Issuer
https://verifiedham.net- Discovery
https://verifiedham.net/.well-known/openid-configuration- Authorize
https://verifiedham.net/authorize- Token
https://verifiedham.net/token- Userinfo
https://verifiedham.net/userinfo- JWKS
https://verifiedham.net/.well-known/jwks.json
Authorization code flow only, with PKCE (S256) required for every client, confidential and
public alike. Confidential clients also authenticate with client_secret_basic or
client_secret_post.
Scopes and claims
| Scope | Claims |
|---|---|
openid | sub: a stable, opaque account ID. It never changes, even if the user's call sign does. |
callsign | callsign, callsign_country (US or CA), callsign_verified, and callsign_verified_at once verified |
profile | name, given_name, family_name as they appear on the licence |
email | email, email_verified |
license | license_class, license_expires |
offline_access | A refresh token |
Check callsign_verified. A user who has started verifying but not
finished gets their call sign with "callsign_verified": false. That call sign
comes only from the user, and anyone can start verifying any call sign, so treat it as a hint.
Don't use it to grant anything that depends on the person actually holding the licence. The
name and licence claims are only released once the call sign is verified. A user who hasn't
started gets no call sign claims at all.
Key your users by sub, not by call sign: call signs change (vanity calls) and are
eventually reissued to other people. The mailing address is never released.
Example
Send the user to:
https://verifiedham.net/authorize?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://app.example.com/callback
&scope=openid%20callsign
&state=RANDOM
&code_challenge=BASE64URL(SHA256(VERIFIER))
&code_challenge_method=S256
Then exchange the code, sending both the secret and the verifier:
curl https://verifiedham.net/token \
-u YOUR_CLIENT_ID:YOUR_CLIENT_SECRET \
-d grant_type=authorization_code \
-d code=THE_CODE \
-d redirect_uri=https://app.example.com/callback \
-d code_verifier=VERIFIER
A public client sends the same request with client_id in the body instead of
-u, since it has no secret.
The ID token carries the claims for the scopes the user approved:
{
"iss": "https://verifiedham.net",
"sub": "usr_01J9Z5W5Q8XKJ2Y1T0V3N4M6PA",
"callsign": "W7APK",
"callsign_country": "US",
"callsign_verified": true,
"callsign_verified_at": 1760500000,
...
}
Client types
- Confidential: server-side apps. You get a client ID and secret, and use PKCE.
- Public: browser, mobile and desktop apps, which can't keep a secret. You get a client ID only, and use PKCE.
- Client ID Metadata Documents: use the URL of a JSON document you publish as the
client_id, with no registration at all. Until you claim it, users see a warning that the app is unverified. To claim one, add"verifiedham_callsign": "YOURCALL"to the document and claim it here.