Firsty

Search...

Search...

Core concepts

Embedded web app

Sign your own users into Firsty's hosted app with a signed JWT.

Instead of building against the API, you can embed Firsty's own web app in your product. Your users get data plans without a second sign-up, because you stay the authentication authority: you assert who the user is with a short-lived signed JWT. Firsty verifies it, provisions the matching account, and runs the session from there.

A partner opens the embedded web app with a signed token, and the user lands in Firsty already signed in, picks a plan and pays without leaving the partner's product.

How it works

  1. You open the web app at https://web.firsty.app/<partnerCode>?userToken=<JWT>.
  2. The web app exchanges that token with the Firsty backend.
  3. Firsty verifies the signature and claims, resolves or creates the user, and signs them in. Session handling is Firsty's from that point.

You never call the Firsty backend yourself. Your side is two things: configuring how Firsty verifies your tokens, and minting a token per user before opening the URL.

Onboarding

ItemWho provides itNotes
partnerCodeFirstyYour identifier. Goes in the embed URL and the token's iss claim.
Verification modeYouJWKS or shared secret.
Keys or JWKS URLYouA public JWKS endpoint, or the shared secret.
Signing algorithmYouOne of the supported algorithms below.

The partnerCode is the same in every environment, but verification config is provisioned per environment, so keys can differ. Confirm with Firsty which key belongs to which.

Verifying your tokens

Both modes require TLS 1.2 or higher.

You host a public JSON Web Key Set, for example at https://your-domain.com/.well-known/jwks.json. You sign with your private key; Firsty fetches the matching public key to verify. Rotation is entirely yours to manage: publish a new key and it's live, with no secret to exchange.

Each key's kid must match the kid in the header of tokens signed with it.

{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "alg": "PS256",
      "kid": "your-key-id",
      "n": "<base64url-modulus>",
      "e": "AQAB"
    }
  ]
}

Supported: PS256, PS512, ES256, ES512, EdDSA.

Shared secret

Firsty and you exchange a secret over a secure channel, and it both signs and verifies. Supported: HS256, HS512.

Firsty can hold several secrets at once and accepts a token signed by any of them, so rotation needn't take downtime: add the new secret, move your signer to it, then retire the old one.

The user token

Mint a JWT per signed-in user.

ClaimMeaningConstraints
subYour identifier for the user10 to 128 characters. Letters, digits, - and _ only.
issIssuerExactly your partnerCode.
audAudienceExactly firsty.app.
iatIssued atSeconds since epoch.
expExpirySeconds since epoch. Keep it short; 5 minutes is recommended.
campaignOptional signup couponMax 128 characters, and must name a campaign Firsty has provisioned. See below.

sub is the identity. Firsty recognises the same user across sessions by it, and it's how you find them in the partner area, so it has to be stable for a given user and never reused across two. The token is passed as a URL query parameter, which is the other reason to keep the lifetime short and generate it at the moment you open the embed URL. A small clock skew between your servers and Firsty is tolerated.

Header, signing asymmetrically. Include kid so Firsty can pick the right key:

{
  "alg": "PS256",
  "typ": "JWT",
  "kid": "your-key-id"
}

Payload:

{
  "sub": "usr8f3kd92mzQ",
  "iss": "yourPartnerCode",
  "aud": "firsty.app",
  "iat": 1751280000,
  "exp": 1751280600,
  "campaign": "summer-2026"
}

For shared-secret mode the payload is identical, alg is HS256 or HS512, and no kid is needed.

Opening the web app

Put the partnerCode in the path and the fresh token in userToken:

https://web.firsty.app/<partnerCode>?userToken=<JWT>

Load it in a WebView on mobile, or an iframe or new tab on web. Firsty exchanges the token and establishes the session. A user who comes back while that session is still valid needs no new token; otherwise open the URL again with a fresh one.

EnvironmentHost
Productionhttps://web.firsty.app
Test (UAT)https://web.test.firsty.app

Both are provisioned during onboarding, each with its own verification config, so sign with the key that belongs to the host you're opening. Validate the whole flow on test before you point at production.

Signup coupons

Firsty can attach a coupon to a partner user, at signup or on a later login. You ask for it with the campaign claim, and Firsty reserves a code and applies it at checkout on the user's behalf. There's no separate API call; the claim is part of the signed token, so it can't be tampered with in transit.

A campaign has to exist before you can name one. Ask your Firsty representative or get in touch, and Firsty provisions the following per environment:

ItemNotes
Campaign nameThe exact string you send in the campaign claim.
DiscountType (free days, fixed amount, percentage) and value.
Active periodThe coupon only applies while the campaign is running.
Code poolPregenerated codes. Each user reserves one; an exhausted pool assigns nothing.

How it behaves:

  • Reserved when the user has no coupon yet. First time Firsty sees the claim for a (partnerCode, sub) pair, and again on a later login if that user still holds none. A user created before the campaign existed can be given one by sending the claim again.
  • One coupon per user, across all campaigns. If they already hold one, the request reuses it. Sending a different campaign name does not swap it.
  • Applied automatically at checkout, while the campaign is active. The user never types a promo code.
  • Active campaigns only. An expired or not-yet-started campaign assigns nothing, even with codes left in the pool.

Accounts

Every partner user gets their own Firsty account, keyed on (partnerCode, sub). First sight of a pair provisions it, and every later token with the same pair resolves to that same user, so returning users keep their data.

Partner accounts stay separate from native Firsty users and from other partners' users. They are never merged, even when they share a phone number or email address. The embedded experience covers data plans; voice calling isn't part of it.

When it fails

SituationResult
Signature invalid, token expired, or claims fail validationUnauthorized. Mint a new, valid token.
Unknown partnerCode, or config not provisioned for that environmentUnauthorized. Confirm onboarding for the environment you're opening.
Too many verification attempts in a short windowThrottled. Back off and retry.
campaign names an unknown, inaccessible or depleted campaignLogin succeeds, no coupon assigned.

Claim validation covers the sub format and length, aud being exactly firsty.app, iss matching your partnerCode, and the token lifetime not being too long. If tokens are rejected and the claims look right, check that the signing algorithm and key match what Firsty provisioned for that environment.

Anything else, get in touch or ask in your shared Slack channel if you have one.