WandamarkBetaDocs
OpenAPIDashboard

Connect your apps to Wandamark

Three ways in, use any mix per app: post events to the REST API (or the TypeScript SDK), let Wandamark poll your app's MCP server, or forward webhooks from Sentry, Stripe and RevenueCat. Your AI tools can then manage everything through the Wandamark MCP server.

Quickstart

  1. Create a workspace and click Add app. Copy the server key (wm_live_…) and client key (wm_pub_…).
  2. Send a session when a user opens your app, and forward bug and abuse reports:
curl -X POST https://wandamark.com/api/v1/events \
  -H "Authorization: Bearer wm_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "events": [
    { "type": "session", "user_id": "u_123" },
    { "type": "user_report", "reason": "harassment",
      "reported_user_id": "u_456", "user_id": "u_123",
      "content": "Threatening messages in DMs" }
  ] }'

The response tells you what was accepted: {"accepted": 2, "rejected": []}. Data shows up on the dashboard immediately.

Keys and auth

  • Server key wm_live_…: per app, backend only. Can send every event type.
  • Client key wm_pub_…: per app, safe to ship in iOS, Android, desktop and web builds. Can send sessions, signups, crashes, bug reports, user reports and releases; it can't send revenue or metrics.
  • Access token wm_pat_…: per person, for the MCP server and management API. Acts with your role. Create one in Settings.

Send keys as Authorization: Bearer <key>. Keys are stored hashed; rotate them under Integrations → Manage.

Events API

POST https://wandamark.com/api/v1/events. Body: { "events": [ ... ] } (up to 500), a single event, or an array. Every event takes optional timestamp (ISO-8601, defaults to now). CORS is enabled for client keys.

typefieldswhat it does
sessionuser_idA user opened the app. Drives DAU, MAU, retention and the crash-rate denominator.
activeuser_idMark a user active without counting a session.
signupuser_idA new account. Counted once per user.
identifyuser_id, user_name?, user_created_at?Attach a display name or real account creation date (used for account age in reports).
crash—One crashed session. Crash rate = crashes ÷ sessions over 7 days.
bug_reporttitle, description?, severity?, device?, app_version?, user_id?, contact?, fingerprint?, external_id?A bug from a user or tracker. Same fingerprint (default: title) de-duplicates and counts affected users.
user_reportreported_user_id, reason, content?, user_id?, reporter_name?, reported_user_name?, reported_user_created_at?, priority?, context?, external_id?A user reporting another user (harassment, scam, impersonation, inappropriate content, hate speech, spam, other).
revenueamount | amount_cents, currency?, fee?, customer_id?, subscription_id?, plan?, period_start?, period_end?Server key only. USD only; convert other currencies before sending. Negative amounts record refunds. A service period (e.g. an annual plan) is recognized month by month in statements.
subscriptioncustomer_id, plan, price | price_cents, subscription_id?, interval?, interval_count?, status?, started_at?, current_period_end?Server key only. The current state of one subscription; send it on create, plan change and cancel. Drives subscribers per plan, MRR and churn.
metricname, valueServer key only. name: dau, mau, store_rating, crash_rate, uptime, retention_d30.
daily_totalsdate, dau?, mau?, signups?, sessions?, crashes?, revenue?Server key only. Idempotent daily aggregates when you already compute them.
uptimeup (boolean), status_code?, latency_ms?Server key only. Your own health probe results.
releaseversionCurrent app version shown on the dashboard.

Check a user's moderation status from your backend: GET https://wandamark.com/api/v1/users/{user_id}/status with the server key returns {"status": "active" | "warned" | "suspended" | "banned", "suspended_until": …}.

TypeScript SDK

The TypeScript SDK batches events, retries with backoff and works in browsers, React Native and Node. It's a single dependency-free file: download wandamark.ts and add it to your project.

import { Wandamark } from "./wandamark";

const wm = new Wandamark({ key: "wm_pub_…", endpoint: "https://wandamark.com" });

wm.session(user.id);                       // daily & monthly actives
wm.reportBug({ title: "Crash on launch", severity: "critical", userId: user.id });
wm.reportUser({ reportedUserId: "u_456", reason: "harassment", userId: user.id });

Paid user testing

Fund a test under User testing (your budget ÷ testers = each tester's reward, plus a 10% fee). Testers open your app from a Wandamark link that carries a wm_test code. Call startTest() on every launch: it does nothing for everyone else, and for testers it records pages, clicks, rage clicks (3 clicks on the same element within 1 second), dead clicks (no page, DOM or scroll change within 3 seconds), JavaScript errors and active time, and shows a small task bar. Automatic capture omits keystrokes, input values, element text and error messages. Add data-wm-mask to hide an element and its descendants. Use data-wm-label for a non-sensitive click label; keep IDs and URL paths free of personal data. Click positions (as fractions of the page) feed the heatmap. The task bar is a checklist of every task; testers can also leave notes and file bug reports with severity, steps, recent error messages and an optional screenshot they can redact before sending. Screen and voice recording, when a test uses it, runs on the tester's Wandamark session page, so your app needs no extra code.

const wm = new Wandamark({ key: "wm_pub_…", endpoint: "https://wandamark.com" });
const test = wm.startTest();          // web: reads ?wm_test=… ; native: wm.startTest({ code })

// Browser task markers are participation signals, not trusted payment evidence
test?.completeTask("t2");             // task ids are t1, t2, … in the order you wrote them
test?.screen("Checkout");             // React Native and other non-browser apps
test?.note("Couldn't find the size chart");

After the tasks, testers rate each one (Single Ease Question, 1–7), answer the System Usability Scale and Net Promoter Score, and write what they liked and what got in the way. Review each submission within 7 days. Only unflagged work auto-approves; missing or conflicting evidence needs human review. The AI report classifies every comment with TypeSafe's Jev decision model (issue, praise or request; product area; Nielsen severity 0–4; task) and groups them into themes with testers' own quotes. Apple doesn't allow paying TestFlight testers, so link to your App Store or web version.

For backend-verifiable tasks, bind the tester code to the authenticated app user, derive the outcome from your own records and send a stable receipt before submission. Never forward a browser claim of success as server evidence. Negative feedback is not fraud.

await server.testEvidence({
  code: user.boundTestCode, taskId: "t2",
  eventId: "test-order:" + order.id, outcome: "completed"
});

Cash sales commissions

Fund a sales campaign under Paid referrals. Signup establishes attribution for $0. Cash rewards require genuine paid purchases confirmed by your backend, a positive USD minimum and at least a 30-day holdback. Clicks, free signups, installs and engagement do not earn commissions. Each step pays once per referred customer, within the campaign window and prepaid budget.

// Browser: send this code to your backend with the signup.
const ref = wm.referralCode();
// Server: establish attribution after verifying the account.
const server = new Wandamark({ key: "wm_live_…", endpoint: "https://wandamark.com" });
await server.referralSignup({ code: ref, userId: user.id, email: user.verifiedEmail });
// After verifying the paid order with your payment processor:
await server.referralSale({
  userId: user.id, orderId: order.id, amount: order.paidCents / 100,
  currency: "USD", buyerEmail: user.verifiedEmail
});

Use the same order ID and business payload on every retry. Report refunds through the referral refund API. Reward ladders can distinguish a minimum per sale from cumulative verified payments. The full ladder is reserved at attribution, and unearned reservations expire. Only canonical sale events can pay cash; browser analytics can still record other actions.

Affiliates must disclose their commissions. Testers and affiliates receive funds through their own approved Stripe accounts. Campaign funding and new transfers require recorded provider approval for that activity, approved countries and matching platform configuration. Paid usability research is approved separately from sales commissions.

Connect your app's MCP server

If your app already exposes an MCP server (Streamable HTTP), choose MCP server when adding the app. Wandamark connects every 5 minutes with your bearer token and calls whichever of these tools exist. Reports and bugs are de-duplicated by id, so returning the same items again is safe.

// get_metrics → JSON object (all fields optional)
{ "dau": 18200, "mau": 241000, "store_rating": 4.6, "crash_rate": 0.4,
  "uptime": 99.95, "retention_d30": 31, "version": "4.2.0",
  "daily": [{ "date": "2026-10-02", "dau": 18100, "signups": 420,
              "sessions": 51000, "crashes": 210, "revenue": 1890.50 }] }

// list_user_reports → array (or { "items": [...] })
[{ "id": "r_981", "reason": "harassment", "content": "…",
   "reporter_id": "u_1", "reported_user_id": "u_2",
   "reported_user_name": "@nightowl", "created_at": "2026-10-02T09:12:00Z" }]

// list_bug_reports → array
[{ "id": "b_77", "title": "Crash on launch", "severity": "critical",
   "device": "iPhone 15 · iOS 18.3", "app_version": "4.2.0",
   "affected_users": 318, "created_at": "2026-10-01T22:10:00Z" }]

// list_subscriptions → array (current state of each subscription; USD)
[{ "id": "sub_9", "customer_id": "cus_42", "plan": "Pro Annual", "price": 89.99,
   "interval": "year", "status": "active", "started_at": "2026-03-04T10:00:00Z" }]

Wandamark MCP server

Endpoint https://wandamark.com/api/mcp (Streamable HTTP, stateless). Tools: list_apps, get_app_metrics, list_bug_reports, update_bug, list_user_reports, moderate_report, create_milestone, get_revenue. Ask things like “which of my apps need attention?” or “triage today's critical bugs in Pulse”.

claude mcp add --transport http wandamark https://wandamark.com/api/mcp --header "Authorization: Bearer wm_pat_…"
{
  "mcpServers": {
    "wandamark": {
      "type": "http",
      "url": "https://wandamark.com/api/mcp",
      "headers": { "Authorization": "Bearer wm_pat_…" }
    }
  }
}

Management API

Authenticate with an access token (wm_pat_…). Full schema: OpenAPI 3.1.

GET    https://wandamark.com/api/v1/apps                        # apps + metrics
POST   https://wandamark.com/api/v1/apps                        # { name, platform, connection?, mcp_url?, … } → keys
GET    https://wandamark.com/api/v1/bugs?app=<id>&status=Open
PATCH  https://wandamark.com/api/v1/bugs/<id>                   # { status?, severity?, assignee_id?, notify_on_fix? }
GET    https://wandamark.com/api/v1/reports?app=<id>&status=Open
POST   https://wandamark.com/api/v1/reports/<id>/actions        # { action: dismiss|warn|suspend|ban|reopen, note?, suspend_days?: 1-90 }

Webhooks to your app

Set a webhook URL on the app so it can enforce decisions made in Wandamark. Events: moderation.action, bug.updated, bug.fixed (includes the reporters to notify). Failed deliveries retry with exponential backoff (6 attempts over about an hour). Verify every request:

{
  "id": "8d5c…",
  "type": "moderation.action",
  "created_at": "2026-10-03T12:00:00Z",
  "app_id": "…",
  "data": {
    "report_id": "…", "ref": "RPT-5121",
    "action": "suspend",            // dismiss | warn | suspend | ban | reopen
    "user_id": "u_456",             // the reported user in your app
    "reason": "Harassment",
    "suspended_until": "2026-10-10T12:00:00Z",
    "note": "Repeated threats in DMs"
  }
}
import { createHmac, timingSafeEqual } from "node:crypto";

// Wandamark-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
export function verifyWandamark(rawBody: string, header: string, secret: string) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));
}

Subscriptions and revenue

The Revenue page (paid plans) shows MRR, ARR, subscribers per plan, churn, upgrades and trial conversions. Financial statements (Growth and Unlimited) add a monthly revenue statement, MRR and subscriber roll-forwards and deferred revenue, exportable as PDF, CSV or XLSX and emailed on the 1st as a monthly close.

  • Stripe: enable customer.subscription.created, .updated, .deleted, invoice.paid and charge.refunded.
  • RevenueCat: purchases, renewals, product changes, billing issues and expirations update subscriptions automatically (App Store and Google Play).
  • Your MCP server: add list_subscriptions returning the current state of each subscription.
  • REST/SDK: send a subscription event whenever a subscription starts, changes plan or ends (wm.subscription()).
{ "type": "subscription", "customer_id": "cus_42", "subscription_id": "sub_9",
  "plan": "Pro Annual", "price": 89.99, "interval": "year", "status": "active",
  "started_at": "2026-03-04T10:00:00Z" }

Statuses: trialing, active, past_due and paused, canceled, expired. MRR is the price normalised to a month (annual ÷ 12, weekly × 52 ÷ 12) for active and past_due subscriptions. Repeating the same state is safe; older snapshots are ignored. Statements are management reports from billing data and are not audited.

Sentry, Stripe and RevenueCat

Connect the integration under Integrations, then point the tool's webhook at https://wandamark.com/api/v1/integrations/{sentry|stripe|revenuecat}?key=<server key>. Where the tool can send a header (RevenueCat), leave ?key off and send Authorization: Bearer <server key> instead, so the key stays out of URLs and logs. Sentry issues become bug reports (de-duplicated per issue), Stripe invoice.paid/charge.refunded and RevenueCat purchases/refunds become revenue, and their subscription events keep subscribers and MRR current (see Subscriptions and revenue).

Recipes

  • Mixpanel or Amplitude: a nightly job that reads yesterday's DAU, MAU and signups and posts one daily_totals event per app.
  • Firebase Crashlytics: post the crash-free percentage as {"type":"metric","name":"crash_rate","value": 100 - crashFree}.
  • App Store / Google Play: post the current rating as a store_rating metric once a day.
  • In-app “Report user” button: call wm.reportUser() with the client key; act on it in the queue; enforce it from the moderation.action webhook.

Limits and errors

  • Up to 500 events and 1 MB per request; 1,200 requests per minute per key; 300 per minute per access token.
  • Errors look like {"error": {"code": "invalid_api_key", "message": "…"}} with status 400, 401, 402 (plan limit), 403, 404, 409 or 429.
  • Invalid events inside a batch don't fail the batch; they're listed in rejected with their index.

Stuck on an integration? Email support@wandamark.com with your app name and the request you're sending (never include keys).