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
| Product | Status | Note |
|---|---|---|
| Analytics & custom events | Supported | |
| Feedback widget | Supported | |
| Review badge | Supported | |
| Web push | With a step | Needs a route at /userlily-sw.js |
| Roadmap embed | Supported | |
| Feedback, survey & roadmap links | Supported |
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>10. Share links
Every campaign, survey and public roadmap also has a hosted page - nothing to install. Find each one under Share in the dashboard.
Hosted pages
Feedback form https://dev.userlily.com/f/YOUR_CAMPAIGN_KEY Survey https://dev.userlily.com/s/YOUR_SURVEY_KEY Roadmap https://dev.userlily.com/r/YOUR_ROADMAP_KEY
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.