Examples and troubleshooting Developer
Last updated Aug 16, 2026
Examples and troubleshooting
Snippets help you match the protocol. Full runnable scripts are in Minimal Python example. Replace constants with your real values.
Discovery document
curl -sS https://api-passport.swaymoon.com/.well-known/openid-configuration | jq .
Confirm fields such as authorization_endpoint, token_endpoint, userinfo_endpoint, jwks_uri, and id_token_signing_alg_values_supported.
Authorize URL (browser redirect)
https://api-passport.swaymoon.com/oauth2/authorize
?response_type=code
&client_id=swm_YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&scope=openid%20name%20picture%20email
&state=YOUR_STATE
&code_challenge=YOUR_CHALLENGE
&code_challenge_method=S256
Encode query parameters only once when you build the URL. If the address bar shows redirect_uri=http%253A%252F%252F…, that is double encoding. The server validates the literal value and fails.
Token exchange: confidential client (curl)
ISSUER=https://api-passport.swaymoon.com
CLIENT_ID='swm_YOUR_CLIENT_ID'
CLIENT_SECRET='YOUR_SECRET'
CODE='...'
VERIFIER='...'
REDIRECT='https://app.example.com/callback'
curl -sS -u "$CLIENT_ID:$CLIENT_SECRET" \
-X POST "$ISSUER/oauth2/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$CODE" \
--data-urlencode "redirect_uri=$REDIRECT" \
--data-urlencode "code_verifier=$VERIFIER"
Token exchange: JWT assertion client (curl)
Sign client_assertion on the backend first (do not put the private key in shell history). Python example:
import time, uuid
from jwcrypto import jwk, jwt # pip install jwcrypto
key = jwk.JWK.from_json(open("private.jwk.json").read()) # private key with d; do not submit to the portal
now = int(time.time())
token = jwt.JWT(
header={"alg": "ES256", "kid": key["kid"], "typ": "JWT"},
claims={
"iss": "swm_YOUR_CLIENT_ID",
"sub": "swm_YOUR_CLIENT_ID",
"aud": "https://api-passport.swaymoon.com/oauth2/token",
"jti": str(uuid.uuid4()),
"iat": now,
"exp": now + 300,
},
)
token.make_signed_token(key)
assertion = token.serialize()
Then:
ISSUER=https://api-passport.swaymoon.com
CLIENT_ID='swm_YOUR_CLIENT_ID'
CODE='...'
VERIFIER='...'
REDIRECT='https://app.example.com/callback'
ASSERTION='...' # JWT from the previous step
curl -sS \
-X POST "$ISSUER/oauth2/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$CODE" \
--data-urlencode "redirect_uri=$REDIRECT" \
--data-urlencode "client_id=$CLIENT_ID" \
--data-urlencode "code_verifier=$VERIFIER" \
--data-urlencode "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
--data-urlencode "client_assertion=$ASSERTION"
aud must match token_endpoint in the discovery document. To refresh, change grant_type to refresh_token and sign a new assertion (new jti / iat / exp).
Token exchange: public client (curl)
ISSUER=https://api-passport.swaymoon.com
CLIENT_ID='swm_YOUR_CLIENT_ID'
CODE='...'
VERIFIER='...'
REDIRECT='https://app.example.com/callback'
curl -sS \
-X POST "$ISSUER/oauth2/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$CODE" \
--data-urlencode "redirect_uri=$REDIRECT" \
--data-urlencode "client_id=$CLIENT_ID" \
--data-urlencode "code_verifier=$VERIFIER"
UserInfo (curl)
curl -sS https://api-passport.swaymoon.com/userinfo \
-H "Authorization: Bearer $ACCESS_TOKEN"
Refresh token (confidential clients)
curl -sS -u "$CLIENT_ID:$CLIENT_SECRET" \
-X POST "$ISSUER/oauth2/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "grant_type=refresh_token" \
--data-urlencode "refresh_token=$REFRESH_TOKEN"
FAQ
| Symptom | Likely cause | What to do |
|---|---|---|
| Authorize error / cannot reach the callback | redirect_uri does not match the registered value, or it is double-encoded | Compare character by character. The address bar should not show %253A / %252F |
invalid_request / PKCE-related errors | Missing code_challenge, or code_challenge_method is not S256 | Use matching code_verifier and code_challenge in authorize and token exchange |
invalid_client | Wrong Basic credentials, Basic / secret used on a public client, or JWT assertion failed verification | Check the type. Confidential: check client_id/client_secret. Public: send client_id in the form. JWT: check kid, public key, aud, iss/sub, exp, and ES256 |
invalid_grant | code already used or expired, or code_verifier mismatch | Start authorization again. Do not resubmit the same code |
| Consent page missing expected scopes | Portal does not have that scope, or the request omitted the matching scope | Check the app’s optional scopes and the scope parameter |
Callback error=invalid_scope | Requested scope is outside the client’s registered set | Check the matching scopes on portal Capabilities, or shrink scope in the request |
UserInfo has no email | Token lacks email (user unchecked optional email, or it was not requested) | Check scope in the token response. When email is granted, the field should always be present |
| UserInfo profile looks “empty” | Ungranted profile fields return default placeholders (nickname Swaymoon User, avatar ""), not missing fields | Use token scope to tell real data from placeholders. See Tokens and user info |
| User chose Hide My Email | email may be a @privaterelay.swaymoon.com relay | Treat it as a normal email. The real address is not given to you |
sub differs from other apps | Expected pairwise behavior | Use sub only within the current client_id or team |
| ID Token signature fails | Not using ES256, or cached JWKS is stale | Fetch /oauth2/jwks again and confirm alg is ES256 |
Frontend exposes client_secret | Confidential client used for a pure SPA | Create a public client, or keep the secret only on the backend |
Private JWK (with d) in the frontend or git | Private key submitted as a public key, or a key file was committed | The portal accepts public keys only. Rotate kid and retire the leaked private key |
JWT assertion invalid_client | aud is not token_endpoint, kid is not registered, or the assertion used RS256 / is expired | Use the token URL from discovery. Header kid must be registered. Algorithm must be ES256 |
Public client has no refresh_token | Expected | After the Access Token expires, run authorization code + PKCE again |
Integration tips
- Take endpoints from the discovery document; do not hard-code paths.
- Full local scripts: Minimal Python example.
- Sign-in and authorization confirmation happen in the browser. Confidential and JWT assertion clients should exchange tokens and call UserInfo on the server.
- Production Issuer and frontend host are
api-passport.swaymoon.comandpassport.swaymoon.com.
Getting help
- User docs: turn off Developer Mode, then see the Guides and Legal categories.
- Developer portal: develop.swaymoon.com
- Contact: hello@swaymoon.com (do not send plaintext
client_secretthrough public channels; mask it when you describe a problem)