Ideapreneur Nepal logoSuperPreneur for Developers v1.0

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.