Revvly
Docs
API Reference

Send product events to see how clients use your product.

Report what your users do (a report exported, a feature opened) and Revvly shows it on each client’s Usage tab, in the cross-client overview and in the support ticket of the person who wrote in. Nothing here is billed: for metered, billable usage use the Event Ingestion API.

Authentication

A Bearer API key that holds PRODUCT_EVENT_WRITE (“Can send product events”). No existing key has it: grant it when you issue a key. It is not the same as USAGE_WRITE, and a key for one cannot call the other. The workspace is the key’s own; nothing in the body names a tenant.

Authorization: Bearer rvly_sk_live_<your-token>
Content-Type:  application/json
iThe key never goes in a page. It is a secret and must not ship in client code. Send from your backend with the key, or, for what only a browser sees (pages, time on page), have your backend mint a short-lived token for the signed-in person and send with that (see Browser tracking below).

#Send events

POST /api/v1/product-events: 1 to 100 events per request, answered 202 with one result per event. One bad event is refused on its own and never sinks the others.

event
Required. The event name, up to 128 characters. Each new name appears in Settings → Product events, where an admin says what it means.
userId / clientId
At least one. Your own ids for the person and for their company; the client id is matched to the client’s external id in Revvly. Up to 255 characters.
eventId
Optional. Your own id; sending the same one again returns DUPLICATE and stores nothing, so retries are safe. Without one, Revvly makes an id and a retry cannot be recognised.
timestamp
Optional ISO 8601. Defaults to now. Up to 15 minutes ahead and 30 days back.
properties
Optional flat object, up to 50 keys; values are strings (500 characters), numbers, booleans or null. Nested values are refused: send them as separate properties.
Request
curl -X POST https://api.revvly.ai/api/v1/product-events \
  -H "Authorization: Bearer rvly_sk_live_<your-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "eventId": "evt_8f2c",
        "event": "report.exported",
        "userId": "u_1042",
        "clientId": "acme",
        "timestamp": "2026-11-01T09:30:00Z",
        "properties": { "format": "pdf", "rows": 240 }
      }
    ]
  }'
Response
{
  "results": [
    { "eventId": "evt_8f2c", "status": "ACCEPTED" }
  ]
}

Each result is ACCEPTED, DUPLICATE or REJECTED with a reason that says what to fix.

iSecrets are not stored. A property whose name reads as a credential (password, token, secret, api key, card number, cvv, iban) is stored as [redacted], as is any string value that is an email address, a JWT or a valid card number. The key is kept so the property still shows in the catalogue; the result lists the names it replaced in redacted. Send ids, not personal data.

Events are kept for 13 months, then removed.

#Mint a browser token

POST /api/v1/product-events/tokens: your backend asks for a token for the person who is signed in. It takes a userId and/or a clientId (at least one) and an optional ttlSeconds (60 to 3600, default 900), and needs the same PRODUCT_EVENT_WRITE key. Hand the token to the page and mint a new one before it expires. Revoking the key stops every token it minted.

curl -X POST https://api.revvly.ai/api/v1/product-events/tokens \
  -H "Authorization: Bearer rvly_sk_live_<your-token>" \
  -H "Content-Type: application/json" \
  -d '{ "userId": "u_1042", "clientId": "acme", "ttlSeconds": 900 }'

# { "token": "eyJ...", "expiresAt": "2026-11-01T10:15:00.000Z" }

#Send events from the browser

POST /api/v1/product-events/browser with the token as a Bearer credential. Same events, validation and masking as the server route, at most 50 per request, and an optional sessionId that groups a visit.

Who
The person and the workspace are the token’s. An event that names a different userId or clientId is refused with 403; one that names none is stamped with the token’s.
Allowed sites
The page’s origin must be listed in Settings → Product events → Browser tracking (scheme and host, for example https://app.example.com; or https://*.example.com for every subdomain, which does not include example.com itself or a white-label domain: list those on their own lines; up to 100). Any other site gets 403, so a token handed to the wrong page is useless.
Limits
10 requests a second and 300 a minute per person. Over that the answer is 429 with a Retry-After header.
// In the page, with the token your backend handed it.
await fetch('https://api.revvly.ai/api/v1/product-events/browser', {
  method: 'POST',
  keepalive: true, // lets the request finish while the page closes
  headers: {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    events: [
      {
        event: 'page.viewed',
        sessionId: 's_7f3a',
        properties: { path: '/reports' },
      },
    ],
  }),
});

Browser SDK

One script that does the browser half for you: it records a page.viewed on load and on every single-page navigation, a page.left with the seconds the page was actually visible (activeSeconds), and whatever you track. Events carry a session id that changes after 30 minutes without activity, so Revvly can show sessions, session length and top pages.

There is no package to install: Revvly serves the script from the address you sign in to the dashboard at, and a page loads the current version each time. The step-by-step setup (key, clients, allowed sites, token route) is in Set up product event tracking.

<script src="https://<your-revvly-address>/revvly-events.js"></script>
<script>
  RevvlyEvents.init({
    // Ask YOUR backend for a token: it calls POST /api/v1/product-events/tokens
    // with its API key. Called again before the token expires.
    getToken: () => fetch('/my-backend/revvly-token').then((r) => r.text()),
  });

  // Page views and time on each page are recorded for you. Add your own:
  RevvlyEvents.track('report.exported', { format: 'pdf' });
</script>
Pages, not records
The query string and hash never leave the browser, and ids (numbers, UUIDs, long hex or token-like segments) become :id, so /invoices/8123 is reported as /invoices/:id. Other segments go out as they are (a username or a short code in a path is not recognised), so if your paths hold anything personal name your routes yourself with routePattern, a function from path to route. A hash-routed app reports every page as /, because the hash is dropped. The page title is not sent unless you pass sendTitle: true; titles often hold names or record numbers. A referrer from another site is reduced to the site, one from your own site is its cleaned path, and the session id is kept in this browser's local storage under revvly_events_session.
Delivery
Events go out in small batches every few seconds and when the page is hidden or closed. A network error or a 5xx is retried with backoff, a refused token is replaced by asking your backend again, and every event has an id, so a retry never counts twice. If Revvly is away for long, the oldest events are dropped rather than the page slowed. A request that gets no answer in 15 seconds, or a token call that gets none in 10, is given up and tried again later, and if your token function fails or returns anything but the token string the console says so once.
Privacy
RevvlyEvents.optOut() stops sending, clears what is queued and remembers the choice in that browser; optIn() reverses it. Pass respectDoNotTrack: true to send nothing when the browser says Do Not Track.
Options
apiUrl (default https://api.revvly.ai), autoPageViews (default true, turn it off if you report pages yourself with pageView(path)), sessionTimeoutMinutes, flushIntervalMs, maxBatch and sendTitle (default false).

The two events it makes, page.viewed and page.left, appear in Settings → Product events like any other; leave them as they are to feed the session views.

#Erase one user

DELETE /api/v1/product-events/users/:userId removes every stored event of that user (a data-subject request), in your workspace only. Same key and capability; it answers the number deleted and cannot be undone.

curl -X DELETE https://api.revvly.ai/api/v1/product-events/users/u_1042 \
  -H "Authorization: Bearer rvly_sk_live_<your-token>"

# { "eventsDeleted": 312 }