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

SymptomLikely causeWhat to do
Authorize error / cannot reach the callbackredirect_uri does not match the registered value, or it is double-encodedCompare character by character. The address bar should not show %253A / %252F
invalid_request / PKCE-related errorsMissing code_challenge, or code_challenge_method is not S256Use matching code_verifier and code_challenge in authorize and token exchange
invalid_clientWrong Basic credentials, Basic / secret used on a public client, or JWT assertion failed verificationCheck 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_grantcode already used or expired, or code_verifier mismatchStart authorization again. Do not resubmit the same code
Consent page missing expected scopesPortal does not have that scope, or the request omitted the matching scopeCheck the app’s optional scopes and the scope parameter
Callback error=invalid_scopeRequested scope is outside the client’s registered setCheck the matching scopes on portal Capabilities, or shrink scope in the request
UserInfo has no emailToken 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 fieldsUse token scope to tell real data from placeholders. See Tokens and user info
User chose Hide My Emailemail may be a @privaterelay.swaymoon.com relayTreat it as a normal email. The real address is not given to you
sub differs from other appsExpected pairwise behaviorUse sub only within the current client_id or team
ID Token signature failsNot using ES256, or cached JWKS is staleFetch /oauth2/jwks again and confirm alg is ES256
Frontend exposes client_secretConfidential client used for a pure SPACreate a public client, or keep the secret only on the backend
Private JWK (with d) in the frontend or gitPrivate key submitted as a public key, or a key file was committedThe portal accepts public keys only. Rotate kid and retire the leaked private key
JWT assertion invalid_clientaud is not token_endpoint, kid is not registered, or the assertion used RS256 / is expiredUse the token URL from discovery. Header kid must be registered. Algorithm must be ES256
Public client has no refresh_tokenExpectedAfter 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.com and passport.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_secret through public channels; mask it when you describe a problem)