How-to Guide

Everything you need to put Morvero in your products: install the widget, collect feedback and usage data, and turn it into portfolio decisions.

Morvero is portfolio-level product intelligence for enterprise software: usage analytics, feature-level adoption, in-product feedback, NPS, and AI-drafted backlogs across every internal app and AI-agent pilot — flat-priced per portfolio (never per-user or per-event), anonymous by design, installed with one script tag per product. Feedback flows to Jira or Azure DevOps as tickets with acceptance criteria and cited user evidence.

Reading this as an AI agent? The same content is served as clean Markdown at /docs.md (also /llms-full.txt); the short machine-readable summary is /llms.txt.

On this page Getting started Install the widget What your users see Track pages & functions Countries & world map Mobile apps & REST API Track AI agent pilots Connect AI coding agents AI backlog & themes Read the dashboards Team, sign-in & passkeys Plans Privacy & GDPR Security & abuse protection Troubleshooting

1. Getting started

  1. Create a workspace — click Start free, enter your company name and work email. We email you a single-use sign-in link; clicking it verifies your address and opens the workspace — nothing is accessible before that. No password: you'll be invited to add a passkey (Face ID / Touch ID / security key) right after that first sign-in, and email links always work as a fallback.
  2. Create a product — in the console go to Products → + New product. Give it a name, choose a random-sampling rate (what share of visitors gets a proactive feedback prompt), and optionally restrict which domains may use the key.
  3. Copy the snippet — Morvero hands you one line of HTML. That's the whole integration.
  4. Follow the setup checklist — new workspaces open with a "Get Morvero live" checklist on the Overview: create a product, install the snippet (Morvero watches live for your first pageview), collect your first feedback, invite a teammate. Progress is read from your real data — never a stored checkbox — and the card retires itself once you're live. Dismissible any time.

2. Install the widget

Paste the snippet into the product's footer or layout template so it renders on every page:

<script async src="https://YOUR-MORVERO-HOST/widget.js"
        data-feedback-key="fbk_your_product_key"></script>

3. What your users see

4. Track pages & functions

Pages — automatic

Every page load and SPA route change is recorded as an anonymous pageview. Nothing to do.

Hash-routed SPA? If your app routes with the URL hash (#/invoices, #products) rather than the path, add data-feedback-hash-routing to the script tag:
<script async src=".../widget.js" data-feedback-key="fbk_..." data-feedback-hash-routing></script>
The hash then counts as part of the route, so each view is its own page (and function clicks and feedback are attributed to the view they happened on). It's opt-in because on an ordinary site the hash is an anchor link — treating those as routes would split one page into dozens.

Functions — tag elements (no JS)

<button data-feedback-action="export-csv">Export CSV</button>

Every click shows up under Most / least used functions.

Functions — from code

MorveroFeedback.track('name') records a function use; MorveroFeedback.open() opens the feedback panel.

Cohorts — optional, privacy-floored

<script async src=".../widget.js" data-feedback-key="fbk_..." data-feedback-cohort="finance"></script>

Pass a coarse group label (department, region, role family) and the product page gains a usage-and-sentiment breakdown by cohort. Aggregate-only with a k-anonymity floor: cohorts under 5 visitors in the selected range are withheld entirely, so the label can never single out a person. Never pass usernames or employee ids.

Two rules: API calls must be in their own <script> tag — a tag with src ignores its inline content. And because the widget loads async, calls made at page-load time need the queue stub below (calls from click handlers later don't).
<script>
  window.MorveroFeedback = window.MorveroFeedback || {
    q: [],
    track: function (n) { this.q.push(['track', n]); },
    open: function () { this.q.push(['open']); }
  };
  MorveroFeedback.track('uptime-page-viewed');   // queued, replayed when the widget loads
</script>
<script async src="https://YOUR-MORVERO-HOST/widget.js" data-feedback-key="fbk_..."></script>

Countries — automatic

Every pageview and function event is stamped with the visitor's country — nothing to configure and nothing extra sent by the widget. The server resolves the request's IP address to a two-letter country code the moment the event arrives (in-process, using a bundled GeoLite2 country database — no third party sees the IP) and stores only the code; the IP itself is never written anywhere. The Overview shows a world heat map of where your users are, and each product page gets a Geography card breaking down which pages and functions are used from which country. Events collected before this feature existed, or from IPs that can't be resolved (e.g. corporate VPN egress ranges not in the database), appear as Unknown. If you front Morvero with Cloudflare or a similar CDN, its country header is used instead — it's fresher than any bundled database.

Mobile apps & REST API

The web widget is just a client of a small public REST API — native iOS and Android apps (and server-side integrations) call the same two endpoints directly. No SDK to install, nothing to bundle. The full surface (collection API + Enterprise read API) is machine-readably specified at /openapi.json.

The flow: token, then submit

  1. Fetch a submission tokenGET /api/widget-config?key=fbk_your_product_key. The response includes a short-lived token (valid ~15 minutes). Fetch it right before submitting; if a queued submission gets a 401 token_invalid, fetch a fresh token and retry.
  2. SubmitPOST /api/collect/feedback with a JSON body.
# 1. token
curl "https://YOUR-MORVERO-HOST/api/widget-config?key=fbk_..." \
  -H "X-Morvero-App-Id: com.company.app"

# 2. submit
curl -X POST "https://YOUR-MORVERO-HOST/api/collect/feedback" \
  -H "Content-Type: application/json" \
  -H "X-Morvero-App-Id: com.company.app" \
  -d '{
    "key": "fbk_...",
    "token": "TOKEN_FROM_STEP_1",
    "rating": 4,
    "comment": "Love the new checkout, but Apple Pay failed once.",
    "path": "/checkout",
    "title": "Checkout",
    "platform": "ios",
    "visitorId": "STABLE_ANONYMOUS_UUID"
  }'

Drop-in native SDKs (optional)

If you'd rather not hand-roll the calls, two single-file, dependency-free wrappers implement the whole flow — anonymous visitor id persistence, the X-Morvero-App-Id header, and the token fetch + retry for feedback. Add the file to your app target and go:

// Swift
MorveroFeedback.configure(origin: "https://YOUR-MORVERO-HOST", key: "fbk_...", appId: "com.company.app")
MorveroFeedback.trackScreen("/invoices")
MorveroFeedback.trackAction("export-csv")
MorveroFeedback.submitFeedback(rating: 4, comment: "Works well")

// Kotlin
MorveroFeedback.configure(context, origin = "https://YOUR-MORVERO-HOST", key = "fbk_...", appId = "com.company.app")
MorveroFeedback.trackScreen("/invoices")
MorveroFeedback.submitFeedback(4, "Works well")

Every call is fire-and-forget on a background thread and never crashes the host app. Both files support the optional cohort label (see Cohorts).

Restrict the key to your apps

API keys are public by design. On the web you pin them with allowed domains; for native apps set allowed app IDs (product page → Mobile apps & REST API) and send yours in the X-Morvero-App-Id header on every call. The rules:

Like a browser Origin, the header is a declaration, not proof — it stops casual key reuse, not a determined attacker. The per-visitor caps and token expiry below are the real abuse controls.

Swift (iOS)

struct MorveroFeedback {
    static let host = "https://YOUR-MORVERO-HOST"
    static let key = "fbk_..."
    static let appId = "com.company.app"

    static func submit(rating: Int, comment: String, screen: String) async throws {
        // 1. token
        var cfg = URLRequest(url: URL(string: "\(host)/api/widget-config?key=\(key)")!)
        cfg.setValue(appId, forHTTPHeaderField: "X-Morvero-App-Id")
        let (cfgData, _) = try await URLSession.shared.data(for: cfg)
        let token = (try JSONSerialization.jsonObject(with: cfgData) as! [String: Any])["token"] as! String

        // 2. submit
        var req = URLRequest(url: URL(string: "\(host)/api/collect/feedback")!)
        req.httpMethod = "POST"
        req.setValue("application/json", forHTTPHeaderField: "Content-Type")
        req.setValue(appId, forHTTPHeaderField: "X-Morvero-App-Id")
        req.httpBody = try JSONSerialization.data(withJSONObject: [
            "key": key, "token": token, "rating": rating, "comment": comment,
            "path": screen, "platform": "ios", "visitorId": anonymousId(),
        ])
        _ = try await URLSession.shared.data(for: req)
    }
}

Kotlin (Android)

suspend fun submitFeedback(rating: Int, comment: String, screen: String) {
    val host = "https://YOUR-MORVERO-HOST"; val key = "fbk_..."; val appId = "com.company.app"
    val http = OkHttpClient()

    // 1. token
    val cfg = Request.Builder().url("$host/api/widget-config?key=$key")
        .header("X-Morvero-App-Id", appId).build()
    val token = http.newCall(cfg).execute().use {
        JSONObject(it.body!!.string()).getString("token")
    }

    // 2. submit
    val body = JSONObject(mapOf(
        "key" to key, "token" to token, "rating" to rating, "comment" to comment,
        "path" to screen, "platform" to "android", "visitorId" to anonymousId(),
    )).toString().toRequestBody("application/json".toMediaType())
    val post = Request.Builder().url("$host/api/collect/feedback")
        .header("X-Morvero-App-Id", appId).post(body).build()
    http.newCall(post).execute().close()
}

Good to know

Track AI agent pilots

Enterprises pilot dozens of internal AI agents — support copilots, HR bots, SQL assistants — and nobody can say which are used, liked, or worth continuing. Morvero answers that the same way it does for products: each agent pilot is a product with usage, sentiment, outcomes and cost on one scorecard.

Quickstart

  1. Create a product with type "AI agent pilot" (Products → + New product). Agent products get relabeled dashboards (conversations, active users, capabilities), a pilot scorecard, and a row in the Overview's AI agent pilots table. Assign a tribe and Product Owner like any product.
  2. Instrument the agent's backend with three HTTPS calls (no token needed for usage telemetry; feedback needs the usual short-lived token):
Agent eventMorvero callNotes
Conversation startedPOST /api/collect/pageviewpath = entry surface (/slack, /web-chat, /vscode)
Capability / tool callPOST /api/collect/actionaction = capability name; optional cost in USD (tokens etc.)
Task outcomePOST /api/collect/actionreserved names: task-completed, task-failed, escalated-to-human — these drive the success/escalation KPIs
User thumbs 👍/👎POST /api/collect/feedbackthumb: "up"|"down" (or rating 1–5), comment, platform: "agent"
Periodic CSATPOST /api/collect/nps0–10, same as web NPS

The pilot scorecard

With the conventions above, each agent product page shows task success rate (completed ÷ decided), escalation rate (escalations ÷ conversations), cost per conversation and cost per successful task; the Overview table ranks every pilot side by side. Fund the leaders, fix the frustrating, retire the unused — with evidence.

Drop-in emitter

All the instrumentation you need is this class (Node 18+, zero dependencies). Framework adapters below just decide when to call it.

class MorveroAgent {
  constructor({ host, key, userId }) {
    Object.assign(this, { host, key, userId });
  }
  #post(path, body) { // fire-and-forget: telemetry must never break the agent
    fetch(this.host + path, {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ key: this.key, visitorId: this.userId, ...body }),
    }).catch(() => {});
  }
  conversationStarted(surface = '/chat') { this.#post('/api/collect/pageview', { path: surface }); }
  capability(name, cost = 0, surface = '/chat') { this.#post('/api/collect/action', { action: name, cost, path: surface }); }
  taskCompleted() { this.capability('task-completed'); }
  taskFailed() { this.capability('task-failed'); }
  escalated() { this.capability('escalated-to-human'); }
  async thumbs(direction, comment = '', surface = '/chat') {
    const cfg = await (await fetch(`${this.host}/api/widget-config?key=${this.key}`)).json();
    this.#post('/api/collect/feedback', { token: cfg.token, thumb: direction, comment, path: surface, platform: 'agent' });
  }
}

const morvero = new MorveroAgent({ host: 'https://YOUR-MORVERO-HOST', key: 'fbk_...', userId: 'emp-8f2a' });

Framework adapters

Claude Agent SDK / Anthropic tool loop — count a conversation per session, a capability per tool call, and cost from usage:

morvero.conversationStarted('/support-copilot');
for await (const message of agentSession) {
  if (message.type === 'tool_use') morvero.capability(message.name);
  if (message.usage) morvero.capability('turn', costFromUsage(message.usage)); // your $/token math
}
morvero.taskCompleted(); // or taskFailed() / escalated() per your outcome logic

LangChain (JS) — a callback handler:

const handler = {
  handleChainStart: () => morvero.conversationStarted('/langchain-app'),
  handleToolStart: (tool) => morvero.capability(tool.name),
  handleLLMEnd: (out) => morvero.capability('turn', costFromUsage(out.llmOutput?.tokenUsage)),
};
await chain.invoke(input, { callbacks: [handler] });

OpenAI Agents (Python) — same three calls with requests; fire from your run hooks:

import requests
def emit(path, **body):
    try: requests.post(f"{HOST}{path}", json={"key": KEY, "visitorId": user_id, **body}, timeout=2)
    except Exception: pass

emit("/api/collect/pageview", path="/hr-bot")                      # on run start
emit("/api/collect/action", action=tool_name, cost=turn_cost_usd)  # on tool call
emit("/api/collect/action", action="task-completed")               # on success

Framework APIs move fast — treat these as shapes, not gospel: anywhere you can hook "run started / tool called / run ended", Morvero can score the pilot.

Sharing dashboards outside Morvero

Triage feedback

Connect AI coding agents (MCP)

Morvero ships a remote MCP server, so AI coding agents can read your feedback data and instrument your codebase themselves. Add it to Claude Code:

claude mcp add --transport http morvero https://morvero.com/mcp

On first use the agent opens your browser: sign in to Morvero (passkey or email link), review the consent screen, click Allow. The connector then acts with your role and workspace — OAuth 2.1 with PKCE, tokens stored hashed, revocable by deactivating your account's grants. Works with any MCP client that supports Streamable HTTP + OAuth (Claude Code, Claude.ai connectors, Cursor…). Working examples for every integration surface — including a portable install skill for coding agents — live at github.com/Morvero/examples.

What the agent can do

ToolWhat it's for
list_products / create_productFind or create the product a codebase belongs to (apps and AI-agent pilots)
get_install_snippetReady-to-paste widget snippet or agent-telemetry calls, with the real API key and install notes
get_instrumentation_guidePlatform guides: web, iOS, Android, AI-agent conventions
list_feedbackReal user comments & ratings, filterable — "what are users complaining about?"
list_backlog / get_backlog_itemAI-suggested backlog with acceptance criteria and the verbatim user evidence — implement what the team accepted
get_product_analyticsUsage/sentiment totals + agent-pilot KPIs for prioritization
verify_installConfirms data is actually arriving after instrumenting — distinguishes working / silent / never-received and names the likely blocker. The agent's definition of done

Prompts that work well

The connector can read feedback and keys and create products, but it can never read attachments beyond what your role allows, touch billing, or manage the team — it holds your role, nothing more.

AI: theme summaries & backlog suggestions

Both features appear only when your Morvero server is configured with an AI API key (Anthropic or OpenAI), and both require Growth or Enterprise. AI output assists reading — the raw feedback always remains the source of truth.

5. Read the dashboards

6. Team, sign-in & passkeys

7. Plans

PlanPriceProductsAdminsMax sampling
StarterFree225%
Growth$99/mo flat151025%
Scale$499/mo flat502550%
Enterprisefrom $15,000/yr flat150–unlimitedUnlimited100%

Always flat per portfolio — never per-user or per-event. Owners move between the self-serve tiers under Settings → Plan & usage; Enterprise is sales-assisted. Limits are enforced by the API, with a clear upgrade message when you hit one. Full comparison: pricing.

8. Privacy & GDPR

9. Security & abuse protection

10. Troubleshooting

Feedback isn't arriving

Function tracking isn't recording

Sign-in emails aren't arriving