Signed Handoff
Signed Handoff is the server-side bridge for apps that already have users.
The app authenticates the user, chooses the active account/profile, signs that identity on the server, and sends it to Boomin. Boomin links that external user to a partner program member and starts Instagram OAuth when needed.
When to use it
Section titled “When to use it”Use Signed Handoff when your product already has login:
- SaaS users joining a creator or affiliate program.
- Member profiles that should own referral rewards.
- Apps with Clerk, Supabase, Auth0, Better Auth, or custom auth.
Use the browser OTP flow when the creator is entering from a public page and does not already have an account in your app.
Install
Section titled “Install”npm install @boomin/servernpx @boomin/cli handoff init --framework next --auth customFor the full referral-first starter surface, use:
npx @boomin/cli referral init --framework next --auth custom --writeThat generates the join route plus a partner status route, /r/[code] click tracker, and a /partner UI that shows the user their referral link and metrics.
Next.js route
Section titled “Next.js route”import { createBoominCreatorJoinHandler } from "@boomin/server/next";
async function getCurrentUser(request) { const user = await yourAuthGetCurrentUser(request); if (!user) return null; return { externalUserId: user.id, email: user.email, name: user.name || user.email, metadata: { source: "custom" }, };}
export const GET = createBoominCreatorJoinHandler({ publicKey: process.env.BOOMIN_CONNECT_PUBLIC_KEY, programId: process.env.BOOMIN_CONNECT_PROGRAM_ID, redirectUri: process.env.BOOMIN_CONNECT_REDIRECT_URI, signingSecret: process.env.BOOMIN_HANDOFF_SIGNING_SECRET, issuer: "your-app.com", loginUrl: "/login", getCurrentUser,});Your frontend button stays simple:
<a href="/api/boomin/creator/join">Join creator program</a>Payload
Section titled “Payload”The server signs stable JSON with HMAC-SHA256:
{ "iss": "your-app.com", "aud": "boomin.ai", "iat": 1779980000, "exp": 1779980300, "nonce": "uuid", "publicKey": "pk_live_...", "programId": "program-id", "redirectUri": "https://your-app.com/creator-program", "externalUserId": "user_123", "email": "creator@example.com", "name": "Creator Name", "metadata": {}}Boomin verifies the signature, consumes the nonce once, validates the redirect URI, links externalUserId to a partner, and returns either an Instagram authUrl or the existing member status.
Security rules
Section titled “Security rules”- Never expose
BOOMIN_HANDOFF_SIGNING_SECRETin browser code. - Handoff payloads expire in 5 minutes by default.
- Nonces are one-time use.
- Redirect URIs must match the program’s allowed redirect origins.
- Public keys are browser-safe; handoff signing secrets are server-only.
Atlantium reference
Section titled “Atlantium reference”Atlantium signs the active Better Auth profile:
externalUserId = atlantium_profile_<profile_id>email = Better Auth user emailname = active profile display namemetadata = { atlantiumUserId, atlantiumProfileId, profileType }That lets Atlantium users join the creator program without entering a second email OTP.
Referral-first status
Section titled “Referral-first status”Signed handoff responses and standing responses include a stable referral surface:
{ "referral": { "code": "KLEVELAND42", "url": "https://your-app.com/r/KLEVELAND42", "active": true }, "metrics": { "linkClicks": 4, "signups": 1, "sales": 0, "gmvCents": 0, "productUsage": 0 }, "approvalStatus": "pending", "qualificationStatus": "qualified", "requiredChannels": [], "missingChannels": []}If a program requires Instagram, missingChannels includes instagram until the user connects it. Referral links still exist before channel connection.
Channel requirements drive the flow
Section titled “Channel requirements drive the flow”The handoff response’s status is computed from the surface’s requiredChannels:
requiredChannels: ["instagram"]— a member without a connected Instagram getsstatus: "needs_instagram". Send them through the channel connect step before treating them as pending.requiredChannels: []— the Instagram step is skipped entirely and members land atpending_approvalwith no connected channel.
A surface created by handoff provision starts with requiredChannels: []. If your program expects creators to connect Instagram, set it explicitly after provisioning with the Platform API (or the Program Operator MCP pack):
curl -X POST https://api.boomin.ai/v1/platform/programs/connect-config/update \ -H "Content-Type: application/json" \ -d '{ "token": "sk_boomin_live_...", "programId": "<programId>", "requiredChannels": ["instagram"] }'Do not show “Instagram is connected” copy on pending_approval alone — check the member’s connection state. With requiredChannels set this cannot normally diverge, but belt-and-suspenders UI copy stays honest.
Referral link base
Section titled “Referral link base”referral.url is built from the surface’s referral base. If unset it falls back to https://boomin.ai/r/<code>, which is not your app’s click tracker — point it at the click-through route on your own domain (the /r/[code] route the referral scaffold generates) via connect-config metadata:
curl -X POST https://api.boomin.ai/v1/platform/programs/connect-config/update \ -H "Content-Type: application/json" \ -d '{ "token": "sk_boomin_live_...", "programId": "<programId>", "metadata": { "referralBaseUrl": "https://your-app.com/r" } }'npx @boomin/cli doctor warns when the referral route, referral base, or destination env are missing.
Keeping the link on your domain is deliberate: affiliates share it in Instagram bios, Telegram, and Discord, and a brand-domain URL is what people trust enough to click. The route works from any of those — it is a plain HTTP redirect that records the click server-side with your signing secret, so no cookie or JavaScript is required at click time.
Your app appears as a channel
Section titled “Your app appears as a channel”Provisioning a handoff config for a real issuer automatically installs your app as a channel on that brand’s Channels page in Boomin, with live program metrics (clicks, signups, GMV, member counts). Smoke/test issuers (.local, .invalid, .test, .example) are ignored.