Quick Start
Get Marli Running in One Afternoon
TL;DR — the integration is a single iframe, authenticated by short-lived JWTs your backend signs with a private key. Three steps, roughly one afternoon of engineering.
1Generate Your Signing Keypair
Everything you need is on your Channel Partner page in Lytica (Account → Channel Partner). There is no welcome packet to wait for and nothing to email us.
Click Generate Keypair. We create an RSA keypair server-side, store the public half against your partner record automatically, and show you the private half exactly once — save it to your secrets manager right then. You never send us a key.
The same page shows the values the snippets below need:
- Partner slug — kebab-case identifier (e.g.
iscream) - Embed URL —
https://alpha.lyticalabs.ai/embed/cp/{slug} - Key ID (kid) — stamped into the JWT header you sign.
One thing still needs us: approved iframe origins. Until your production and staging hosts are on that list the embed is served with frame-ancestors 'self' and browsers will block it everywhere except the Lytica test page. Send us the exact origins (e.g. https://app.example.com) before you ship.
An API key is optional. It is only for server-to-server customer-org provisioning; most partners skip it during pilot because customer records are created on first use.
2Wire Up Your Backend (Mint Endpoint)
Expose one endpoint that the SDK calls from the browser. Use our createMarliMintHandler helper for ~10 lines of code:
import { createMarliMintHandler } from '@lyticalabs/marli-sdk/server';
export const POST = createMarliMintHandler({
partnerSlug: process.env.CP_PARTNER_SLUG!,
privateJwkJson: process.env.CP_PARTNER_PRIVATE_JWK!,
kid: process.env.CP_PARTNER_KID!,
alg: 'RS256',
resolveContext: async (request) => {
const session = await getYourSession(request);
if (!session) return null;
return {
partnerOrgId: session.activeOrg.id,
userId: session.user.id,
};
},
});Set three env vars: CP_PARTNER_SLUG, CP_PARTNER_KID, and CP_PARTNER_PRIVATE_JWK.
3Mount the Widget in Your Frontend
'use client';
import { MarliWidget } from '@lyticalabs/marli-sdk/react';
export function MarliPanel() {
return (
<MarliWidget
baseUrl="https://alpha.lyticalabs.ai"
partnerSlug="your-slug"
onMintJwt={async () => {
const res = await fetch('/api/marli-token', { method: 'POST' });
if (!res.ok) throw new Error('mint failed');
const { jwt } = await res.json();
return jwt;
}}
iframeAttrs={{
title: 'Marli',
allow: 'clipboard-read; clipboard-write',
style: { width: '100%', height: '600px', border: 'none' },
}}
/>
);
}4Provision a Customer Org (Optional)
You can skip this. A customer org is created the first time you mint a JWT carrying its partnerOrgId, which is why the API key in Step 1 is optional. Make this call when you want the row to exist before anyone chats, or to give it a display name:
curl -X POST https://alpha.lyticalabs.ai/api/v1/cp/orgs \
-H "Authorization: Bearer ${CP_PARTNER_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"partnerExternalOrgId": "publisher_12345", "name": "Acme Media"}'Idempotent — calling twice with the same ID returns 409 (org already exists).
5Test It
Open your product, send "ping" to Marli. You should see a response stream in within ~2 seconds. Then run the full pre-launch production checklist before going live.
JWT Cheat Sheet
Your JWT has exactly five payload claims: iss (your slug), iat, exp (5–15 min TTL), partnerOrgId, and userId.
Important: partnerOrgId and userId must not contain the colon character (:). If your IDs use colons, URL-encode or replace them consistently.