J.Jupid Docs
Embed for partners

Next.js

Next.js route handler example for signing Jupid Embed tokens.

Use a server-only route handler to sign the token. The exact authentication method depends on the partner app; the route must only run for an authenticated partner user.

Set these values in the partner application's .env.local. Use the same partner ID for the server and client, and replace both staging URLs in the component below with the Jupid app URL provided to you.

JUPID_EMBED_PARTNER_ID="your-partner-id"
JUPID_EMBED_SECRET="your-server-only-shared-secret"
NEXT_PUBLIC_JUPID_EMBED_PARTNER_ID="your-partner-id"

Install a JWT signer

npm install jose

Route handler

Create app/api/jupid/embed-token/route.ts. The getCurrentUser import below represents your app's authentication: replace it with your own implementation. It must return the signed-in user's string ID, email, and non-empty display name, or no user when signed out.

This example's company_name is generic partner-specific metadata. Replace payload with the contract agreed with Jupid. For the HTTP synchronization API, use business access for a business this user can access. Always Bank account snapshots use the separate snapshot contract.

import { SignJWT } from "jose"
import { NextResponse } from "next/server"
import { getCurrentUser } from "@/lib/auth"

const partnerId = process.env.JUPID_EMBED_PARTNER_ID
const secretValue = process.env.JUPID_EMBED_SECRET
if (!partnerId || !secretValue) {
  throw new Error("Missing JUPID_EMBED_PARTNER_ID or JUPID_EMBED_SECRET")
}
const secret = new TextEncoder().encode(secretValue)

export async function GET() {
  const user = await getCurrentUser()
  if (!user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
  }
  const now = Math.floor(Date.now() / 1000)
  const token = await new SignJWT({
    email: user.email,
    name: user.name,
    payload: {
      company_name: user.companyName,
    },
  })
    .setProtectedHeader({ alg: "HS256", typ: "JWT" })
    .setIssuer(partnerId!)
    .setAudience("jupid-embed")
    .setSubject(user.id)
    .setIssuedAt(now)
    .setExpirationTime(now + 300)
    .setJti(crypto.randomUUID())
    .sign(secret)

  return NextResponse.json(
    { token },
    { headers: { "Cache-Control": "no-store" } },
  )
}

The sample also assumes your user object exposes companyName; adapt that metadata to your application. Keep the route on the server and never sign tokens from a Client Component. An authenticated GET now returns {"token":"..."} with caching disabled.

Client component

Create a client component such as app/components/jupid-embed.tsx and render it on an authenticated page. Load the SDK with next/script and use onReady: it runs after the script loads and when the component mounts again after navigation. The cleanup removes the iframe and listener on unmount.

The partner ID is not a secret; expose it with a NEXT_PUBLIC_ env var.

"use client"

import Script from "next/script"
import { useEffect, useRef, useState } from "react"

export function JupidEmbed() {
  const containerRef = useRef<HTMLDivElement>(null)
  const [sdkReady, setSdkReady] = useState(false)

  useEffect(() => {
    if (!sdkReady) {
      return
    }
    const controller = window.JupidEmbed.mount({
      partnerId: process.env.NEXT_PUBLIC_JUPID_EMBED_PARTNER_ID!,
      tokenUrl: "/api/jupid/embed-token",
      container: containerRef.current!,
      initialPath: "/",
      appUrl: "https://jupid-staging-app.example.com",
    })
    return () => controller.destroy()
  }, [sdkReady])

  return (
    <>
      <Script
        onReady={() => setSdkReady(true)}
        src="https://jupid-staging-app.example.com/embed.js"
        strategy="afterInteractive"
      />
      <div ref={containerRef} style={{ height: "100%", minHeight: 720 }} />
    </>
  )
}

Add the browser global in types/jupid-embed.d.ts, included by your tsconfig.json:

export {}

declare global {
  interface Window {
    JupidEmbed: {
      mount(options: {
        partnerId: string
        token?: string
        tokenUrl?: string
        container: HTMLElement
        initialPath?: string
        shell?: "topbar" | "sidebar"
        chat?: "enabled" | "disabled"
        appUrl?: string
      }): {
        destroy(): void
      }
    }
  }
}

After mounting, the iframe opens the user's Jupid workspace. Check both a first visit and navigating away and back. See Browser SDK for destination overrides, the single HTTP 401 retry, and controller behavior.

On this page