Authentication
How partner identity is mapped into a secure embedded Jupid session.
Jupid Embed uses partner-signed JWTs. The partner authenticates the user in its own product, then its server creates a short-lived token for Jupid.
The shared secret must stay on the partner server. It must never be exposed in frontend code, browser bundles, mobile clients, or public repositories.
Required claims
The JWT must be signed with HS256 and include this protected header:
{
"alg": "HS256",
"typ": "JWT"
}{
"iss": "your-partner-id",
"aud": "jupid-embed",
"sub": "partner-user-123",
"email": "user@example.com",
"name": "Jane Founder",
"payload": {
"company_name": "Acme Studio"
},
"iat": 1779360000,
"exp": 1779360300,
"jti": "one-token-id"
}Claim rules
| Claim | Rule |
|---|---|
iss | Partner ID provided by Jupid. |
aud | Must be jupid-embed. |
sub | Durable partner user ID. This is the primary mapping key. |
email | Used when the partner-user mapping is first created. Existing mappings keep their original Jupid auth email. |
name | Display name stored on the Jupid user profile. |
payload | Partner metadata stored by Jupid. |
iat | Issued-at timestamp in seconds. |
exp | Expiration timestamp in seconds. Use roughly 5 minutes. |
jti | Partner-generated token ID. Jupid requires the claim but does not store or consume it. |
Jupid maps partner users by (partner_id, external_user_id), where
external_user_id comes from sub.
Partner payload schema
Partners can use payload for partner-level metadata that Jupid
should receive during session creation. Jupid validates the standard JWT claims
for every partner. For partner-specific fields, agree on a payload schema with
Jupid before production launch.
For a partner that syncs account lists, TypeScript partners can describe the agreed contract with Zod:
import { z } from "zod"
export const partnerPayloadSchema = z.object({
tier: z.enum(["free", "paid"]),
business: z
.object({
id: z.string(),
name: z.string(),
})
.optional(),
accounts: z.array(
z.object({
id: z.string(),
name: z.string(),
number: z.string().optional(),
type: z.string().optional(),
currency: z.string().length(3),
current_balance: z.number().optional(),
plaid_token: z.string().optional(),
status: z.enum(["active", "disabled"]).optional(),
}),
),
})Partners that do not use TypeScript can use an equivalent JSON Schema:
{
"type": "object",
"required": ["tier", "accounts"],
"properties": {
"tier": {
"type": "string",
"enum": ["free", "paid"]
},
"business": {
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
}
},
"accounts": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "name", "currency"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"number": { "type": "string" },
"type": { "type": "string" },
"currency": { "type": "string", "minLength": 3, "maxLength": 3 },
"current_balance": { "type": "number" },
"plaid_token": { "type": "string" },
"status": {
"type": "string",
"enum": ["active", "disabled"]
}
}
}
}
}
}Example account-list payload:
{
"tier": "paid",
"business": {
"id": "business-123",
"name": "Acme Studio"
},
"accounts": [
{
"id": "account-123",
"name": "Operating Checking",
"number": "****1234",
"type": "checking",
"currency": "USD",
"current_balance": 12000.34,
"plaid_token": "<partner-provided-token>"
}
]
}When a partner sends account snapshots, later snapshots should include the full current account list. Accounts missing from a later full snapshot can be treated as disabled. The same payload shape is used by account-change webhooks.
Token lifetime
Jupid checks expiration when the embedded client creates the session. If the token expires before that handoff, Jupid returns HTTP 401 and does not create a session.
A valid token can be replayed until exp. The current implementation
does not provide one-time ticket semantics.
Prefer tokenUrl in the browser SDK. The SDK fetches a fresh token
while mounting and retries once with a newly fetched token after HTTP 401. Do
not cache token endpoint responses.
If a partner passes token directly instead, keep
tokenUrl available for retries. It defaults to
/api/jupid/embed-token. Without a token endpoint, a rejected token
cannot be refreshed by the SDK.
Browser session flow
- The partner page loads
/embed.jsfrom the Jupid app URL for the current environment. - The SDK fetches a signed token from the partner backend.
- The SDK creates an iframe at
JUPID_EMBED_APP_URL/embed/:partnerId. - The iframe asks the parent window for the token.
- The SDK sends the token to the iframe with
postMessage. - Jupid verifies the origin, signature, header, issuer, audience, and expiration.
- Jupid finds or creates the mapped Jupid user.
- Jupid creates a browser session and redirects the iframe.
Origin allowlist
Jupid checks the parent page origin before accepting a token. Send Jupid every local, staging, and production origin that will host the embed in its matching environment.
Example:
http://localhost:3000
https://staging.partner.com
https://app.partner.comJupid also configures iframe frame-ancestor policy for those origins.