Request and response examples
Every sample below was captured from the running system and shortened only where marked. In the commands, $IDP is the identity provider's base URL, $PARTNER is your own public base URL, and $CLIENT_ID, $CLIENT_SECRET, $TOKEN, $NONCE and $SESSION are the values from the earlier step.
1. Get a nonce (your server)#
curl -X POST "$PARTNER/api/nonce"
{ "nonce": "8nZk2Z-Msfpa9GWFqIiP__s6vgeizMt0" }
2. Messages between your page and SuperPreneur#
Your page announces itself with the nonce it just received:
{ "type": "ready", "nonce": "8nZk2Z-Msfpa9GWFqIiP__s6vgeizMt0" }
SuperPreneur answers with the token (shortened here; a real one is about 500 characters):
{ "type": "launch", "token": "eyJhbGciOiJFZERTQSIsImtp...Cjw40TDnu8Dg" }
or, if it refuses:
{ "type": "error", "error": "consent_required" }
3. What is inside the token#
The token is a signed JWT (three Base64URL parts separated by dots). You do not need to read it, because your server learns everything from the verify call. Decoded, the header and payload look like this.
Header:
{ "alg": "EdDSA", "kid": "oGSRh-ZVy5_mJPgS", "typ": "launch+jwt" }
Payload:
{
"iss": "http://localhost:8000",
"aud": "app_vPWas1oF80npj5yb",
"sub": "6wVFC8ZW-uir4EjfnDOwK95teeinCnN1NrjjLgXvNgQ",
"iat": 1791188740,
"exp": 1791188800,
"jti": "04a71232c589771c387d468df6006f11",
"nonce": "8nZk2Z-Msfpa9GWFqIiP__s6vgeizMt0",
"scope": "profile"
}
| Claim | Meaning |
|---|---|
iss |
The identity provider that issued it |
aud |
Your client_id. A token for another app is rejected |
sub |
The user's private id in your app |
iat, exp |
Issued and expiry times in seconds since 1970. exp minus iat is 60 |
jti |
A unique id. The identity provider uses it to make the token work only once |
nonce |
The nonce your server issued for this page load |
scope |
What the user approved, separated by spaces |
The algorithm is EdDSA (Ed25519). The header's kid names the signing key.
4. Create a session (your server)#
Your page sends the token and nonce to your own server:
curl -X POST "$PARTNER/api/session" \
-H "Content-Type: application/json" \
-d '{"token": "'"$TOKEN"'", "nonce": "'"$NONCE"'"}'
{
"session": "FnafyDrLZn...",
"sub": "6wVFC8ZW-uir4EjfnDOwK95teeinCnN1NrjjLgXvNgQ",
"name": "Demo Player"
}
5. Verify a token with the identity provider (your server)#
This is the call that proves who the user is. It must come from your server, because it carries your client secret.
curl -X POST "$IDP/api/launch/verify" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{"token": "'"$TOKEN"'", "nonce": "'"$NONCE"'"}'
A successful answer (200):
{ "sub": "6wVFC8ZW-uir4EjfnDOwK95teeinCnN1NrjjLgXvNgQ", "scopes": ["profile"], "name": "Demo Player" }
The failures you should handle (all captured from real calls):
| Situation | Status | Body |
|---|---|---|
| Same token sent again | 400 | { "error": "replayed" } |
| Wrong nonce (the token stays valid for the right one) | 400 | { "error": "nonce_mismatch" } |
| Not a valid token | 400 | { "error": "invalid_token" } |
| Wrong client secret | 401 | { "error": "invalid_client" } |
Treat any non-200 as "not signed in": do not create a session, and let the page try the handshake again.
6. Call your own API with the session#
curl "$PARTNER/api/bookings" -H "Authorization: Bearer $SESSION"
{ "upcoming": [], "past": [] }
Without a valid session your server answers 401:
{ "error": "no_session" }
7. Optional: check the signature yourself#
The verify call is required. If you also want to look inside the token before you call it, fetch the public key and check the signature. The public keys are at $IDP/.well-known/jwks.json:
{
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"alg": "EdDSA",
"use": "sig",
"kid": "oGSRh-ZVy5_mJPgS",
"x": "sMZWbe0Cf1cFc3PEcU_oJk-Gl9PenmDSj1l-JvNTmwE"
}
]
}
import json, urllib.request
import jwt # pip install pyjwt cryptography
jwks = json.load(urllib.request.urlopen(IDP + "/.well-known/jwks.json"))
kid = jwt.get_unverified_header(token)["kid"]
jwk = next(k for k in jwks["keys"] if k["kid"] == kid)
claims = jwt.decode(
token,
jwt.PyJWK.from_dict(jwk).key,
algorithms=["EdDSA"],
audience=CLIENT_ID, # your client_id
issuer=IDP, # the identity provider's base URL
)
This proves the token is genuine and meant for you. It does not prove it is unused, still consented to, or that the app is still active. Only the verify call knows that, so never skip it. Cache the keys and refetch when you meet an unknown kid, which is how key rotation reaches you.
8. Error format used by SuperPreneur API#
Every error from the identity provider has the same shape: a short machine-readable error, and for most a readable detail.
{
"error": "invalid_request",
"detail": "nonce must be 16 to 128 characters: letters, digits, hyphen and underscore only.",
"fields": { "nonce": ["nonce must be 16 to 128 characters: letters, digits, hyphen and underscore only."] }
}
error |
Status | Meaning |
|---|---|---|
invalid_request |
400 | A field is missing or wrong; fields says which |
invalid_json |
400 | The body is not valid JSON |
invalid_client |
401 | Wrong client id or secret |
not_authenticated |
403 | No signed-in user (calls made by SuperPreneur itself) |
permission_denied, app_suspended |
403 | Not allowed, or your app is suspended |
consent_required |
403 | The user has not approved your app |
not_found, unknown_app |
404 | No such route or app |
method_not_allowed |
405 | Wrong HTTP method |
rate_limited |
429 | Too many calls. The body includes retry_after_seconds |
expired, replayed, nonce_mismatch, wrong_audience, invalid_token, revoked |
400 | Why a launch token was refused |
Your own server is free to use any error format, but the same { "error", "detail" } shape keeps your page code simple.