Embed this on your site
Four ways to connect an existing site to this engine. Nothing needs rebuilding — each piece is a snippet you paste in.
1. Franki concierge (one script tag)
Paste it just before the closing </body> tag. Change data-source to your site's name so captures are attributed correctly. The chat runs in an iframe, so it cannot affect the host page's styling.
<script src="https://franchise-growth-engine.vercel.app/franki.js" data-source="your-site" defer></script>Tutor mode (inside a course)
The same script tag with two extra attributes. Franki then answers only within that course's scope.
<script src="https://franchise-growth-engine.vercel.app/franki.js"
data-source="learning-hub"
data-mode="tutor"
data-course="Nhượng quyền căn bản"
defer></script>2. Diagnostic sites — submit the result, redirect to the report
Call this when the user finishes, then send them to the returned report_url. The score is stored anonymized; an email is stored only when a consent box was ticked.
// On diagnostic completion — send the result, then send the user to the report.
const res = await fetch("https://franchise-growth-engine.vercel.app/api/diagnostic/submit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
diagnostic_type: "franchisor", // 'franchise_readiness' | 'franchisor' | 'franchisee'
score: 63, // 0-100
band: "Ready", // optional; derived from score if omitted
subscores: { concept: 78, finance: 45, ops: 52 },
market: "Vietnam", // country/region only — never an address
sector: "F&B",
persona: "franchisor", // brand_owner | franchisor | franchisee | investor
source: "franchisor-readiness", // which site this came from
locale: "vi",
// Optional, and ONLY with an explicit consent tick:
// email: "...", name: "...", consent: true,
}),
});
const { report_url } = await res.json();
window.location.href = report_url;3. Index stat card (for FranPulse and articles)
The card always shows the sample size N next to the figure, and updates itself as data changes. dim accepts: overall, market, sector, persona.
<iframe src="https://franchise-growth-engine.vercel.app/embed/stat?dim=market"
width="100%" height="180" style="border:0"
loading="lazy" title="Asia Franchise Readiness Index"></iframe>Or pull the raw numbers as JSON
curl -s https://franchise-growth-engine.vercel.app/api/index.json4. Verifiable completion certificate
Returns a code, a public verification link, and a PNG of the certificate.
// Issue a verifiable completion certificate.
const res = await fetch("https://franchise-growth-engine.vercel.app/api/certificate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
recipient_name: "Nguyễn Văn A",
course_title: "Nhượng quyền căn bản cho chủ thương hiệu",
course_slug: "franchise-basics",
locale: "vi",
}),
});
const { code, verify_url, image_url } = await res.json();5. "Why this match" for FranchiseMatch
Nothing new to build: Franki is already a conversational endpoint, so you pass the profile–brand pair as the question and render the answer. It comes back with one next step, or null when nothing genuinely fits.
// "Why this match" — FranchiseMatch, powered by Franki's engine.
// Franki is a chat endpoint, so you pass the match as a question and render
// the answer. There is nothing new to build server-side.
const res = await fetch("https://franchise-growth-engine.vercel.app/api/franki", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
source: "franchisematch",
locale: lang, // 'vi' | 'en' — or omit to auto-detect
messages: [{
role: "user",
content:
"Explain in 2-3 sentences why this franchise brand fits this investor.\n" +
"Investor: capital " + profile.capital + ", experience " + profile.experience +
", sector preference " + profile.sector + ", city " + profile.city + ".\n" +
"Brand: " + brand.name + ", sector " + brand.sector +
", investment from " + brand.minInvestment + ".\n" +
"Name one thing they should check before committing.",
}],
}),
});
const { answer, tool } = await res.json();
// answer -> the explanation. tool -> the single best next step, or null.
// Capture works the same as anywhere else: POST /api/lead with consent: true.Non-negotiables when integrating
- Never send an email address unless the user ticked consent. The endpoint rejects it without consent.
- The market field takes a country or region only. Never send an address or a phone number.
- Scores and emails are stored in two separate tables and are not joined.
- Always check res.ok before treating a call as successful — fetch does not throw on a 4xx or 5xx.