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.

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

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.
nameNon-empty display name. Sets the profile name on creation and the partner user's display name.
payloadMetadata matching the agreed partner payload contract.
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.

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

  1. The partner page loads /embed.js from the Jupid app URL for the current environment.
  2. The SDK creates an iframe at JUPID_EMBED_APP_URL/embed/:partnerId.
  3. The iframe asks the parent window for the token.
  4. The SDK fetches a signed token from the partner backend, unless one was supplied directly.
  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 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.com

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

On this page