Launching widgets
The app is the only source of truth for who is signed in. It asks core for a one-time code, gives it to your backend (not your page), and you exchange it with core for the member’s external id. You answer with a short-lived widget token that the app hands to your page.
Member launch
Section titled “Member launch”sequenceDiagram
autonumber
box rgb(227,244,243) FINNS platform
participant APP as FINNS app
participant CORE as Core
end
box rgb(246,241,233) Sota Platforms
participant BE as Your backend
participant W as Your widget page
end
APP->>CORE: POST /api/v1/app/widget-launches (access token)
CORE-->>APP: 201 {code, ttlSeconds: 60}
APP->>BE: POST /api/v1/launch {code, slots: ["home"], context}
BE->>CORE: POST /api/v1/points/widget-launches/redeem {code}
CORE-->>BE: {extId, issuedAt, notAfter}
Note over BE: nowrap:Refuse if the member signed out at or after issuedAt.<br/>Session: random token, store its hash,<br/>expires min(now + 15 min, notAfter)
BE-->>APP: {token, expiresIn, slots: {home: {height, widgets}}}
APP->>W: Load /w/home (WebView or iframe)
W->>APP: hello
APP->>W: init {token, expiresIn, slot, widgets, context}
W->>BE: GET /api/v1/points (Bearer widget token)
BE->>CORE: GET /api/v1/points/customers/{extId}
CORE-->>BE: {extId, balance, seq}
BE-->>W: {balance}
W->>APP: ready {height}
- Redeem once, never retry. A code is single-use; a retry after a timeout would fail anyway. Answer the app
401 launch_failed(code refused) or502 unavailable(core failed); the app fetches a new code. - Never downgrade a failed member launch to a guest launch: that would tell a signed-in member they are signed out.
- Bound the session by
notAfter. Core sets it to the expiry of the app’s own access token, so your session never outlives what core would accept. - The token reaches the page by message only, never in a URL, cookie or storage.
Guest launch
Section titled “Guest launch”Guests see widgets too (“sign in to start earning”). The app launches without a code:
sequenceDiagram
autonumber
box rgb(227,244,243) FINNS platform
participant APP as FINNS app
end
box rgb(246,241,233) Sota Platforms
participant BE as Your backend
participant W as Your widget page
end
APP->>BE: POST /api/v1/launch {slots: ["home"], context}
BE-->>APP: {token: null, expiresIn: 900, slots}
APP->>W: Load /w/home
W->>APP: hello
APP->>W: init {token: null, …}
Note over W: nowrap:Guest view. Anything that needs a member<br/>is refused by your backend, not just disabled in the UI
W->>APP: ready {height}
Keeping the session alive
Section titled “Keeping the session alive”Widget tokens are never extended. The app launches again before the token expires (at expiresIn − min(30 s, lifetime / 4)) and sends the new token to the page. If your API answers 401 first, the page asks for one.
sequenceDiagram
autonumber
box rgb(227,244,243) FINNS platform
participant APP as FINNS app
participant CORE as Core
end
box rgb(246,241,233) Sota Platforms
participant W as Your widget page
participant BE as Your backend
end
W->>BE: POST /api/v1/points/adjust (expired token)
BE-->>W: 401
Note over W: nowrap:Keep the pending action<br/>and its Idempotency-Key
W->>APP: token-expired
APP->>CORE: POST /api/v1/app/widget-launches
CORE-->>APP: {code}
APP->>BE: POST /api/v1/launch {code, …}
BE->>CORE: redeem
CORE-->>BE: {extId, issuedAt, notAfter}
BE-->>APP: {token, expiresIn, slots}
APP->>W: token {token, expiresIn}
W->>BE: Retry with the same Idempotency-Key
BE-->>W: {balance}
The app renews only while the slot is on screen and the app is in the foreground, backing off 5 s, 15 s, 60 s, then every 5 minutes after failures. When the member changes (sign-in, sign-out, a different member), the app discards the page and clears the web view’s storage, then launches afresh.
The launch API you host
Section titled “The launch API you host”POST /api/v1/launch, called by the app directly (CORS for FINNS’s app origins only, no credentials).
// Request{ "code": "q3V0…", // absent for a guest "slots": ["home"], "context": { "platform": "ios", "appVersion": "1.4.0", "locale": "en-ID", "theme": "light", "textScale": 1.0, "screen": { "width": 390, "height": 844, "dpr": 3 } }}// 200{ "token": "…", "expiresIn": 900, "slots": { "home": { "height": 146, "widgets": [ { "id": "points" } ] } } }context is a hint for presentation and widget choice, never for security. Errors: 400 invalid_request, 401 launch_failed, 429 rate_limited, 502 unavailable.
Internal and confidential. Share partner sections only as partner builds (pnpm build:partner).

