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.

  1. Your backend mints a session with your afk_ key, naming the affiliate, the components, and the origins allowed to show them.
  2. That returns a client secret. Hand it to your frontend.
  3. 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

NameShowsAsks them to confirm
affiliateTheir referral link, with clicks, sales and total earned.No
productsYour catalogue with what they earn on each product.No
activityTheir recent sales on this app. Refunds show as negatives.No
balanceTheir wallet: ready to withdraw, pending, settling.Yes
withdrawThe balance, plus a button that opens the real cash-out flow.Yes
identityGets 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.

Surfaceplatformallowed_origins
Website, desktop or phonewebYour page origins
Capacitor, Ionic, CordovawebYour shell origin, e.g. capacitor://localhost
Native iOS or AndroidnativeOmit 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.WebView

Confirming 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
colorPrimarycolor
colorPrimaryTextcolor
colorBackgroundcolor
colorSurfacecolor
colorTextcolor
colorTextSecondarycolor
colorBordercolor
colorSuccesscolor
colorWarningcolor
colorDangercolor
fontFamilyfontFamily
fontSizeBaselength
fontSizeSmlength
fontSizeLglength
fontWeightNormalfontWeight
fontWeightMediumfontWeight
fontWeightBoldfontWeight
lineHeightnumber
letterSpacinglength
borderRadiuslength
borderRadiusSmlength
borderRadiusLglength
borderWidthlength
spacingUnitlength
spacingGaplength
contentPaddinglength
buttonRadiuslength
buttonHeightlength
buttonFontSizelength
buttonFontWeightfontWeight
cardShadowshadow
focusRingColorcolor

Layout 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 fetchClientSecret mints a fresh one each time rather than returning a cached string.
  • "Not enabled". That component was not in components when 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.