Elements
Put Affiliateo inside your own product. Your users get their referral link, watch their earnings and cash out without ever leaving your app.
Try them in the playground before you write any code.
How it fits together
Three steps, and the shape will be familiar if you have used Stripe Connect embedded components.
- Your backend mints a session with your
afk_key, naming the affiliate, the components, and the origins allowed to show them. - That returns a client secret. Hand it to your frontend.
- Our SDK mounts each component as an iframe on your page, on our domain.
Your secret key never reaches the browser, and the affiliate's data never passes through your servers.
Components
| Name | Shows | Asks them to confirm |
|---|---|---|
affiliate | Their referral link, with clicks, sales and total earned. | No |
products | Your catalogue with what they earn on each product. | No |
activity | Their recent sales on this app. Refunds show as negatives. | No |
balance | Their wallet: ready to withdraw, pending, settling. | Yes |
withdraw | The balance, plus a button that opens the real cash-out flow. | Yes |
identity | Gets them verified and set up to be paid. | Yes |
1. Mint a session
From your backend only. The afk_ key must never ship inside a browser bundle or an app binary.
curl -X POST \
"https://affiliateo.com/api/v1/businesses/{slug}/apps/{appId}/affiliates/embed-session" \
-H "Authorization: Bearer afk_live_..." \
-H "Content-Type: application/json" \
-d '{
"email": "alex@example.com",
"components": ["affiliate", "balance", "withdraw"],
"allowed_origins": ["https://app.example.com"],
"appearance": { "colorPrimary": "#1754D8", "borderRadius": "12px" }
}'Returns:
{
"client_secret": "eyJhbGciOiJIUzI1NiJ9...",
"expires_in": 3600,
"platform": "web",
"components": ["affiliate", "balance", "withdraw"],
"allowed_origins": ["https://app.example.com"],
"requires_step_up": ["balance", "withdraw"],
"element_urls": {
"affiliate": "https://affiliateo.com/embed/affiliate/eyJhbGci...",
"balance": "https://affiliateo.com/embed/balance/eyJhbGci...",
"withdraw": "https://affiliateo.com/embed/withdraw/eyJhbGci..."
}
}element_urls are paste-able. Drop one into an <iframe src> to see a component working before you install anything. Sessions last an hour.
2. Serve the secret
One endpoint of your own that mints a session for whoever is logged in. Your key stays on your server.
// Your own endpoint. The afk_ key never leaves your server.
app.post('/api/affiliateo-session', async (req, res) => {
const r = await fetch(
`https://affiliateo.com/api/v1/businesses/${SLUG}/apps/${APP_ID}/affiliates/embed-session`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.AFFILIATEO_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: req.user.email, // who this session is for
components: ['affiliate', 'withdraw'],
allowed_origins: ['https://app.example.com'],
}),
},
)
res.json({ client_secret: (await r.json()).client_secret })
})3. Mount
<div id="affiliateo-link"></div>
<script src="https://affiliateo.com/embed.js"></script>
<script>
const affiliateo = Affiliateo.init({
// Called now, and again whenever a session needs replacing.
fetchClientSecret: () =>
fetch('/api/affiliateo-session')
.then((r) => r.json())
.then((d) => d.client_secret),
})
affiliateo.mount('affiliate', '#affiliateo-link')
</script>fetchClientSecret is a function, not a string, so we can call it again when a session expires. A page left open longer than an hour keeps working.
You never set a height. Elements measure themselves and tell the SDK, which resizes the frame.
Where they run
Elements fill the width you give them and lay out from there, so one integration covers every surface. There is no mobile build.
| Surface | platform | allowed_origins |
|---|---|---|
| Website, desktop or phone | web | Your page origins |
| Capacitor, Ionic, Cordova | web | Your shell origin, e.g. capacitor://localhost |
| Native iOS or Android | native | Omit it |
A native app has no iframe, so it opens an element_urls entry top-level in a WebView instead. Nothing frames it, so there is no origin to name.
// Backend: no origins, because nothing frames a native WebView.
{
"email": "alex@example.com",
"components": ["balance", "withdraw"],
"platform": "native"
}
// iOS: open element_urls.balance in a WKWebView
// Android: open it in android.webkit.WebViewConfirming identity
The components marked above open a small window on affiliateo.com and ask the affiliate to confirm who they are. They sign in, or we email them a code. It lasts 15 minutes, then they are asked once more.
This is not something you can turn off, and the reason is worth knowing: an Affiliateo wallet is per person, not per app. It holds what they have earned across every program they are in, including your competitors'. Your session is enough to show their numbers for your app; it is deliberately not enough to open the wallet.
The window is on our domain so they can see the address bar. Nothing they type there reaches your page, and the credential it produces never enters your JavaScript.
Withdrawing and identity checks then continue in that same window, against the flows we already run. We do not rebuild bank collection or ID capture inside your page, which also means your site never needs camera permission.
Windows open from a tap, never on load, so pop-up blockers leave them alone.
Styling
32 tokens. Pass them when you mint a session so the first paint is already yours, and call updateAppearance() for anything that changes afterwards.
// Restyle mounted elements in place, e.g. on a dark-mode toggle.
affiliateo.updateAppearance({
colorSurface: '#191919',
colorText: '#eeeeee',
colorBorder: '#2e2e2e',
})Anything we do not recognise is ignored and listed back to you in ignored_appearance_keys, so a typo degrades to the default look instead of failing the call.
All 32 tokens
colorPrimarycolorcolorPrimaryTextcolorcolorBackgroundcolorcolorSurfacecolorcolorTextcolorcolorTextSecondarycolorcolorBordercolorcolorSuccesscolorcolorWarningcolorcolorDangercolorfontFamilyfontFamilyfontSizeBaselengthfontSizeSmlengthfontSizeLglengthfontWeightNormalfontWeightfontWeightMediumfontWeightfontWeightBoldfontWeightlineHeightnumberletterSpacinglengthborderRadiuslengthborderRadiusSmlengthborderRadiusLglengthborderWidthlengthspacingUnitlengthspacingGaplengthcontentPaddinglengthbuttonRadiuslengthbuttonHeightlengthbuttonFontSizelengthbuttonFontWeightfontWeightcardShadowshadowfocusRingColorcolorLayout is not a token, on purpose. These are money surfaces, and a restyled confirm button that no longer reads as one is a mis-click we both pay for. For a different arrangement, mount the components separately and lay them out yourself. That is unlimited freedom over structure with none of the risk inside a component.
Who can show your elements
allowed_origins is the list of pages permitted to display them. Exact origins, up to 10, no paths and no wildcards. Anywhere else, the browser refuses to render the frame at all, so nobody can lift your elements onto their own site.
Use http://localhost:3000 while developing. Plain http is only accepted for localhost.
If something is not showing
- Blank frame.Your page's origin is probably not in
allowed_origins. The browser console will say the frame was refused. - "Session expired". Sessions last an hour. Check
fetchClientSecretmints a fresh one each time rather than returning a cached string. - "Not enabled". That component was not in
componentswhen you minted. - "Not an affiliate yet". The email is not enrolled in this app. Elements never enrol anyone.
- Confirmation window blocked. Something is calling it outside a tap, or the browser is blocking pop-ups for your site.
Next
The playground for styling, or the API reference for everything the REST endpoints expose.