Skip to content

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.

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}
  1. 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) or 502 unavailable (core failed); the app fetches a new code.
  2. Never downgrade a failed member launch to a guest launch: that would tell a signed-in member they are signed out.
  3. 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.
  4. The token reaches the page by message only, never in a URL, cookie or storage.

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}

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.

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).