Python (Flask, FastAPI)

Keys in the environment, exposed to Jinja2, and one template included from your base layout. There is no server-side Python package: userlily runs in the visitor’s browser.

Installed with script tags in your templates · Written for Flask 3 or FastAPI, with Jinja2 templates

What works

ProductStatusNote
Analytics & custom eventsSupported
Feedback widgetSupported
Review badgeSupported
Web pushWith a stepNeeds a route at /userlily-sw.js
Roadmap embedSupported
Feedback, survey & roadmap linksSupported

1. Add your keys

As environment variables. Only the keys for products you use - the host is always needed.

Environment

USERLILY_HOST=https://dev.userlily.com
USERLILY_TRACKING_ID=YOUR_TRACKING_ID
USERLILY_CAMPAIGN=YOUR_CAMPAIGN_KEY
USERLILY_BROADCAST_KEY=YOUR_PROJECT_KEY

2. Expose them to templates, and serve the worker

Flask or FastAPI - use the one you run. Both also add the root route the push worker needs.

app.py - Flask

import os

from flask import Flask, send_from_directory

app = Flask(__name__)

USERLILY = {
    'host': os.environ.get('USERLILY_HOST', ''),
    'tracking_id': os.environ.get('USERLILY_TRACKING_ID', ''),
    'campaign': os.environ.get('USERLILY_CAMPAIGN', ''),
    'broadcast_key': os.environ.get('USERLILY_BROADCAST_KEY', ''),
}


@app.context_processor
def inject_userlily():
    return {'userlily': USERLILY}


@app.route('/userlily-sw.js')
def userlily_sw():
    return send_from_directory(
        app.root_path, 'userlily-sw.js',
        mimetype='text/javascript',
    )

main.py - FastAPI

import os
from pathlib import Path

from fastapi import FastAPI
from fastapi.responses import FileResponse
from fastapi.templating import Jinja2Templates

app = FastAPI()
templates = Jinja2Templates(directory='templates')

USERLILY = {
    'host': os.environ.get('USERLILY_HOST', ''),
    'tracking_id': os.environ.get('USERLILY_TRACKING_ID', ''),
    'campaign': os.environ.get('USERLILY_CAMPAIGN', ''),
    'broadcast_key': os.environ.get('USERLILY_BROADCAST_KEY', ''),
}
templates.env.globals['userlily'] = USERLILY


@app.get('/userlily-sw.js')
def userlily_sw():
    return FileResponse(
        Path(__file__).parent / 'userlily-sw.js',
        media_type='text/javascript',
    )

3. Create the template

Jinja2. Each product loads only when its key is set.

templates/userlily.html

{% if userlily.tracking_id %}
<script src="{{ userlily.host }}/v1/tracker.js"
  data-tracking-id="{{ userlily.tracking_id }}" async></script>
{% endif %}
{% if userlily.campaign %}
<script src="{{ userlily.host }}/v1/widget.js"
  data-campaign-id="{{ userlily.campaign }}" async></script>
{% endif %}
{% if userlily.broadcast_key %}
<script src="{{ userlily.host }}/v1/broadcast.js"
  data-project="{{ userlily.broadcast_key }}" defer></script>
{% endif %}

4. Include it in your base template

Just before </body>.

templates/base.html

<!-- templates/base.html -->
<body>
  {% block content %}{% endblock %}

  {% include "userlily.html" %}
</body>

5. Send custom events

From any page. window.userlily exists once the tracker has loaded - well before anyone can submit a form.

In a template

<script>
  document
    .querySelector('#signup-form')
    .addEventListener('submit', function () {
      window.userlily.track('signup_completed')
    })
</script>

6. Show the review badge

The badge renders exactly where the tag sits. Switch on “Enable syndication” on the campaign’s Syndication page first.

In a template

<!-- Where the badge should appear -->
<script src="{{ userlily.host }}/v1/syndication.js"
  data-token="YOUR_CAMPAIGN_KEY" async></script>

7. Host the push worker

The worker must be served from your own domain at /userlily-sw.js, as JavaScript, with no redirect and over HTTPS. Each of these fails silently. The route added above serves it; download the file next to your app.

Terminal, next to app.py or main.py

curl -o userlily-sw.js https://dev.userlily.com/v1/userlily-sw.js

8. Ask for push permission from your own button

Optional. Works when the project’s “When to ask” is set to “Only when your own code asks”.

In a template

<button id="notify-me">Get notified</button>
<script>
  var button = document.querySelector('#notify-me')
  button.addEventListener('click', function () {
    // Exists once broadcast.js has started, and only
    // when "When to ask" is "Only when your own code asks".
    if (window.userlilyBroadcast) window.userlilyBroadcast.prompt()
  })
</script>

9. Embed the public roadmap

Turn on “Enable public roadmap” in Roadmap → Settings first. An Allowed domain on the roadmap limits which sites can embed it.

Where the roadmap should appear

<iframe
  data-userlily-roadmap
  src="https://dev.userlily.com/r/YOUR_ROADMAP_KEY?embed=1"
  title="Product roadmap"
  style="width:100%;height:720px;border:0"
  loading="lazy"
></iframe>
<script>
(function () {
  window.addEventListener("message", function (event) {
    if (event.origin !== "https://dev.userlily.com") return;
    var data = event.data;
    if (!data || data.type !== "userlily:roadmap:height") return;
    var frame = document.querySelector("iframe[data-userlily-roadmap]");
    if (frame && data.height > 0) frame.style.height = data.height + "px";
  });
}());
</script>

Notes

  • There is no server-side package: every product runs in the visitor’s browser, from the template.

Every option, limit and troubleshooting step is in the main docs. Logos are trademarks of their owners and are shown only to indicate compatibility.