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.
Before testing, obtain your partner ID, shared secret, Jupid app URL, and approved host origins from Jupid. Follow the Quickstart to connect your token endpoint to the browser SDK.
Required claims
The JWT must be signed with HS256 and include this protected header:
{
"alg": "HS256",
"typ": "JWT"
}The following example shows the JWT structure with generic partner metadata.
Use the payload agreed with Jupid; company_name alone does not satisfy the
Always Bank snapshot or business-access contract. Generate iat from the
current time and exp five minutes later instead of copying these timestamps.
{
"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 | Non-empty display name. Sets the profile name on creation and the partner user's display name. |
payload | Metadata matching the agreed partner payload contract. |
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.
Business access and server synchronization
Send the person's complete business roster and their selected business:
{
"business_id": "business-123",
"businesses": [
{ "id": "business-123", "name": "Acme Studio", "role": "owner" },
{ "id": "business-456", "name": "Acme Shop", "role": "member" }
],
"tier": "paid"
}Keep sub stable across business switches. All people using the same partner
business ID share one Jupid organization and its existing financial history.
Roles are owner or member; there can be several equal owners, and the
first person is not automatically made owner. The partner manages these roles
and access even for owners.
business_id must occur in the list. Business IDs must be unique and contain
1–200 characters; names contain 1–120 characters after trimming. Every entry
requires id, name, and role. tier is optional and applies to each listed
business under the partner agreement. Do not mix this payload with accounts
or other fields.
Every authorization replaces this person's complete roster for the partner.
Omitted businesses are revoked, while their data and other members are retained.
An empty businesses list with business_id: null revokes all of their access
for this partner. Embedded login then returns HTTP 403.
Your backend can call POST /api/v1/auth/partner before any user opens Jupid,
including to apply an empty roster. It returns the person, selected company
and source IDs plus the full business-to-organization mapping. See the
HTTP API contract.
The same token can open an embedded session and select its company. To switch
from the partner host, destroy and remount the embed with a fresh token, the
same sub, a changed business_id, and the complete roster. HTTP data requests
use their signed selected business and explicit URL; they do not follow a
later UI switch.
Reauthorization is the point when Linker's latest access is applied. Reloading alone does not revoke access, and JWT expiration does not end an existing browser session. Removed memberships fail subsequent server checks; data already displayed or downloaded cannot be withdrawn. An old unexpired JWT can still reapply its roster at authorization. See switching and revoking access for the precise guarantees and token replay limits.
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.
Existing account snapshots
The existing Always Bank snapshot flow sends accounts inside the token
payload. It continues to use the account-list schema below, including its
optional business metadata. It is separate from the business-access payload
above. TypeScript partners can describe that snapshot contract with Zod:
import { z } from "zod"
export const partnerPayloadSchema = z.object({
tier: z.enum(["free", "paid"]),
business: z
.object({
id: z.string().min(1),
name: z.string().min(1),
})
.optional(),
accounts: z.array(
z.object({
id: z.string().min(1),
name: z.string().min(1),
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", "minLength": 1 },
"name": { "type": "string", "minLength": 1 }
}
},
"accounts": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "name", "currency"],
"properties": {
"id": { "type": "string", "minLength": 1 },
"name": { "type": "string", "minLength": 1 },
"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, every snapshot must include the full current account list. Previously mapped accounts missing from a later snapshot are deactivated; their transaction history is retained. 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 creates an iframe at
JUPID_EMBED_APP_URL/embed/:partnerId. - The iframe asks the parent window for the token.
- The SDK fetches a signed token from the partner backend, unless one was supplied directly.
- 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 in staging and production. Register every local, staging, and production origin that will host the embed in its matching environment. Local Jupid development skips this check.
Example:
http://localhost:3000
https://staging.partner.com
https://app.partner.comJupid also configures iframe frame-ancestor policy for those origins.