J.Jupid Docs
Embed for partners

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

ClaimRule
issPartner ID provided by Jupid.
audMust be jupid-embed.
subDurable partner user ID. This is the primary mapping key.
emailUsed when the partner-user mapping is first created. Existing mappings keep their original Jupid auth email.
nameDisplay name stored on the Jupid user profile.
payloadPartner metadata stored by Jupid.
iatIssued-at timestamp in seconds.
expExpiration timestamp in seconds. Use roughly 5 minutes.
jtiPartner-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

  1. The partner page loads /embed.js from the Jupid app URL for the current environment.
  2. The SDK fetches a signed token from the partner backend.
  3. The SDK creates an iframe at JUPID_EMBED_APP_URL/embed/:partnerId.
  4. The iframe asks the parent window for the token.
  5. The SDK sends the token to the iframe with postMessage.
  6. Jupid verifies the origin, signature, header, issuer, audience, and expiration.
  7. Jupid finds or creates the mapped Jupid user.
  8. 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.com

Jupid also configures iframe frame-ancestor policy for those origins.

On this page