Which SDK goes where
| Package | Runs in | Key | Sends |
|---|---|---|---|
@ravenkeep/node | Your backend (Node.js 18+) | rk_secret_... | Signups with attribution, identify, product events, revenue, server exceptions, widget user hashes |
@ravenkeep/browser | Your web app | rk_pub_... | First-touch attribution, browser errors, page views, the support chat and feature request widget |
io.ravenkeep:ravenkeep-android | Your Android app (Android 5.0+), from Maven Central | rk_pub_... | Crashes on the next launch, screens and their engaged time, visits, the events you made public, identify |
Ravenkeep (Swift package) | Your iOS app (iOS 13+); not published yet | rk_pub_... | Crashes on the next launch, the screens your app names and their engaged time, visits, the events you made public, identify |
Most products use both: the browser SDK captures where a visitor came from and hosts the widget, and the backend reports the signup, the events and the revenue that only it can see.
Setting up a project
- Create a project in the console (Settings, then Projects) and pick the currency your revenue is reported in; it can change in Settings, then Projects, then General until the project has revenue or spend. The secret key is shown once: store it in your backend's environment.
- Add the origins of your site and your app under Allowed origins, so the publishable key works there; for an Android or iOS app, add its application id or bundle ID under Apps.
- Copy the identity secret (Identity verification) into your backend's environment, next to the secret key.
- Install the Node SDK on your backend and the browser SDK on your marketing site and your app, from npm (below). The Install tab of the project shows both snippets with your publishable key.
- Report signups, identify customers and track events. The Get started list on the console Overview ticks off the SDK and the first event as they arrive.
Installing the SDKs
Both SDKs are public packages on npm, with no runtime dependencies. Install each one where it runs:
npm install @ravenkeep/node # in your backend
npm install @ravenkeep/browser # in your web apppnpm add @ravenkeep/node # in your backend
pnpm add @ravenkeep/browser # in your web appContacts tie it together
A contact is one customer of your product, keyed by your own user id (userId). The console lists contacts under Customers: a customer in the console is a contact in the API and the SDKs. Events, conversations, votes and emails are attached to a contact, and so is every crash reported with a user id. That is how Ravenkeep shows the customers a crash affects, the revenue behind a feature request, and the campaign that brought a customer in.
- The same
userIdin two projects is two contacts: the project always comes from the key. - A contact carries an email, a name, traits (your own key and value pairs, such as
plan), an account (the company or workspace it belongs to), its first-touch attribution and its lifetime value. - Use a stable id that never changes, not an email address: emails change, ids do not.
Keys
Each project has exactly one key pair, shown in the console under Settings, then Projects, then Keys. Both travel in the apikey header.
| Key | Format | Where | Can |
|---|---|---|---|
| Publishable | rk_pub_<slug>_... | Browser code, Android and iOS apps | Open a widget session, chat, vote, suggest, report errors and crashes, page and screen views and the events you made public. Only from allowed origins and listed apps. |
| Secret | rk_secret_<slug>_... | Your servers only | Everything in the ingestion API. Stored hashed: shown once at creation and once at each rotation. |
- The publishable key is public by design: it ships in your pages and apps. It can only do what a visitor may do, and the events and errors it sends are anonymous unless a verified widget session vouches for the user.
- The secret key can write anything for the project. Keep it in your servers' environment, never in a browser bundle, a mobile app or a repository.
- Each SDK refuses the other key: the browser, Android and iOS SDKs never send a secret key, and the Node SDK never sends a publishable one.
- Check a key with
ping()in the Node SDK: it answers with the project the key belongs to.
Allowed origins
A publishable-key request must send an Origin that matches one of the project patterns (Settings, then Projects, then Allowed origins), or it fails with origin_not_allowed. Secret-key requests come from your servers and skip this check.
| Pattern | Matches |
|---|---|
https://app.example.com | Only https://app.example.com. Not https://www.example.com nor http://app.example.com. |
https://*.example.com | https://www.example.com and https://app.example.com. Not https://example.com, https://a.b.example.com nor https://example.com.attacker.io. |
https://**.example.com | Subdomains at any depth, such as https://a.b.example.com. Not https://example.com nor https://evil-example.com. |
http://localhost:* | Any local port. Not http://127.0.0.1:3000. |
*matches inside one host label or the port;**crosses dots. Patterns are origins: a scheme, a host and an optional port, without a path or a trailing slash.- New projects allow
http://localhost:*and the origin of their site URL. A project holds up to 50 patterns. - The allowlist keeps other websites from using your key in their visitors' browsers. It is not authentication: what protects your data is that the publishable key can only do publishable actions, under rate limits.
Apps
Apps have no origin. A publishable-key request without an Origin must name a listed app in the X-Ravenkeep-App header (the Android SDK sends android:<application id>, the iOS SDK ios:<bundle id>), or it fails with app_not_allowed. List them in Settings, then Projects, then Apps. A request with an Origin is always checked against the allowed origins, whatever else it sends.
- An Android application id or an iOS bundle ID (
com.acme.shop) matches exactly, with no wildcards: a debug build'scom.acme.shop.debugis its own entry. A project lists up to 50 apps, and none by default. - Like the origins, the list keeps your key out of other apps' builds of the SDK. It is not authentication either: an application id or a bundle ID is not secret, so the publishable key's limits are what protect your data.
Identity verification
Widget sessions start in the browser with the public key, so a user id alone proves nothing: anyone could claim to be your customer and read their conversations. Identity verification closes that gap. Your backend, which knows who is signed in, signs the user id with the project identity secret; the browser passes the signature as userHash; Ravenkeep recomputes it and opens the session only when they match.
- The user signs in to your app, and your app asks your backend who they are (for example
GET /api/me). - Your backend answers with the user id and
widgetUserHash(userId):hex(HMAC-SHA256(identitySecret, userId)). - The browser calls
identify({ userId, userHash }); the SDK opens a widget session (POST /v1/widget/session). - Ravenkeep compares the hash in constant time. A match opens a verified session for 12 hours, renewed by the SDK while the page is open.
import { Ravenkeep } from '@ravenkeep/node'
const ravenkeep = new Ravenkeep({
secretKey: process.env.RAVENKEEP_SECRET_KEY,
identitySecret: process.env.RAVENKEEP_IDENTITY_SECRET,
})
app.get('/api/me', (req, res) => {
res.json({ id: req.user.id, email: req.user.email, ravenkeepUserHash: ravenkeep.widgetUserHash(req.user.id) })
})const me = await fetch('/api/me').then((response) => response.json())
const { error } = await ravenkeep.identify({ userId: me.id, userHash: me.ravenkeepUserHash, email: me.email })
if (error?.code === 'identity_verification_failed') console.warn('Check the identity secret on the server')- Verification is required by default. Without a valid hash, the session fails with
identity_verification_failedand nothing is written. - If you turn it off (Identity verification in the project settings), a session without a valid hash still opens, unverified: it never sees the stored profile nor earlier conversations of that customer, only those it started in that browser; its events and errors stay anonymous, and your team sees "Unverified identity" on its messages. An email it sends is kept for replies in its conversations only, never as the customer's address.
- The identity secret is separate from the secret key. Only owners and admins can read or rotate it, and rotating it invalidates every hash at once.
Which values win
- Your backend wins. A name, an email or traits set with the secret key (
signupandidentifyin@ravenkeep/node) are never overwritten from the browser. A browseridentifyonly fills a name or an email the contact does not have yet. - Only trusted emails get campaigns. An email set with the secret key, or sent by a verified widget session (a valid
userHash), can receive campaign emails. An email sent from the browser without identity verification is stored as unverified: your team can still answer in the widget, but campaign flows skip that contact with the reasonuntrusted_email. - Campaign emails speak the customer's language. The unsubscribe footer and page use the contact trait
locale(enorpt-BR, set from your backend), else the project's default language (Settings, then Projects, then Email sending).
Attribution from your site to your app
Attribution answers "which campaign brought this customer?". The browser SDK records the first touch of each visitor (UTM parameters, click ids, referrer, landing page) on the first page they open. Your signup form sends it to your backend, and your backend reports it with signup. Ravenkeep writes it once on the contact and never overwrites it.
- Load the browser SDK on your marketing site and in your app, with the same publishable key.
- When they live on different subdomains (
www.example.comandapp.example.com), set the sameattribution.cookieDomain(.example.com) on both: the first touch recorded on the site is read in the app. A referrer from your own domain counts as internal navigation, not as a source. - If the signup happens on another domain altogether, forward the UTM parameters in the link to it, or keep the landing URL and parse it on the server with
parseAttribution(url, referrer, { siteHost }). - Your signup request carries
getAttribution(); your backend passes it untouched tosignup.
import { Ravenkeep } from '@ravenkeep/browser'
const ravenkeep = Ravenkeep.init({
publishableKey: globalThis._importMeta_.env.VITE_RAVENKEEP_KEY,
attribution: { cookieDomain: '.example.com' }, // shared by www. and app.
})
await fetch('/api/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, attribution: ravenkeep.getAttribution() }),
})app.post('/api/signup', async (req, res) => {
const user = await users.create(req.body)
await ravenkeep.signup({ userId: user.id, email: user.email, attribution: req.body.attribution })
res.status(201).json({ id: user.id })
})The Attribution report (Metrics, then Attribution) groups signups, paying customers, revenue, spend, CAC and LTV:CAC by utm_source, then by utm_campaign. A signup campaign flow can filter on the same fields. Signups without a UTM source are grouped under "No UTM source".
Events and naming
Events are the moments that matter to your operations team. Name them once, well: the name is what you pick in Metrics, in flow triggers and as campaign goals, and renaming an event later splits its history in two.
object.action, lowercase, with the action in the past tense:ticket.created,file.uploaded,document.created,members.invited.snake_casefor objects of several words:share_link.created,api_key.rotated.- No ids or personal data in names.
file.uploadedwith{ sizeMb: 12 }, neverfile.uploaded.8f2c. A project keeps at most 1,000 distinct event names; past it, events with a new name are refused (event_name_limit). - Small, categorical properties you will want to compare:
{ plan: 'free' },{ priority: 'high' }, a size bucket rather than an exact size.
Send events from your backend with track and a userId: that ties them to a contact and lets them start flows. Names starting with $ are Ravenkeep's own ($signup, $revenue, $identify, $pageview, $screen, and the time spent on a page or screen, $pageleave and $screenleave). With the publishable key, the browser SDK sends $pageview and $pageleave, the Android and iOS SDKs $screen and $screenleave (the Android SDK since 0.1.0; the iOS SDK's first version is not published yet), and all of them only the names in the project's public event names (Settings, then Projects, then the project's Install tab, under Public events); send everything else from your backend.
Pages and screens
Two reserved events feed the Pages & screens report in Metrics. $pageview is a page of a website or web app: the browser SDK sends it with the page's url, path, referrer and title (autoPageviews: true, or trackPageview()), and the report ranks its path (when you send $pageview yourself, path is text, such as /pricing; any other type is refused). Ids in a path collapse into one row (/item/1 and /item/2 rank as /item/*), and so does a segment holding an e-mail address. $screen is a screen of a mobile or desktop app: properties.screen names the screen and is required (a string of at most 200 characters, trimmed), and the report ranks it as you named it: #, ? and spaces are part of a name, only a /-separated segment that is an id collapses. Name screens the way your app's routes are named (Settings, Lesson, Item), not with the id of what they show; send the id as another property.
ravenkeep.track('$screen', { userId: user.id, properties: { screen: 'Lesson', platform: 'android', lessonId: lesson.id } })- Patterns. Ids in an address or a screen name become
*, so/item/1and/item/2rank as one/item/*row: numbers, UUIDs, hex ids of 8 characters or more with a digit, and runs of 16 or more letters and digits mixed. Words and slugs stay (/blog/top-10-tips). The query string and the fragment are ignored, and a trailing slash too. - Visitors are people: each identified contact once, and each device without one once, by its anonymous id. A device that identifies during the period counts twice: once for its views before, once as the contact.
- Unique users are identified contacts only, as everywhere in Ravenkeep.
- Platform filters web pages (
$pageview) or app screens ($screen); a property filter keeps the views whose property has a value, such asplatformequalsios. - Time columns show the engaged time reported for each row in the period (below): its total, and the average per view that reported it. With a property filter they are empty: the time is reported apart from the view, without its properties.
Time on pages and screens
How long a page or screen held the person is reported as an event when the view ends: $pageleave with the page's path (the browser SDK sends it since 0.3.0, with autoPageviews: true), or $screenleave with the screen's screen, named as its $screen names it (the Android and iOS SDKs send it; any app can send it through the API, and your backend with the Node SDK). Both carry engaged_ms, the engaged time in milliseconds: an integer from 0 to 1,800,000 (30 minutes). The browser SDK measures it from 0.3.0, the Android SDK the same way from 0.1.0, and the iOS SDK from its first version, not published yet. An app that sends $screenleave itself follows the rules below, so web and apps compare:
- Engaged means visible (the page shown, the app in the foreground) and in use: time counts for at most 60 seconds after the view was shown or the person last acted (a tap, a click, a key, the wheel, media playing with sound). A page read for 30 seconds without a click reports about 30 seconds; a hidden tab, an idle screen, a page that scrolls itself or a muted video adds nothing.
- Segments. The time since the last report is sent when the next view replaces this one (before that view, so it stays in its visit), when the page is hidden or the app goes to the background, and every 15 minutes while the view holds time. Later segments of the same view carry
resumed: true, so the views measured are the leave events without it (send it only astrue:falsereads as absent, a first segment). - Not metered, but bounded.
$pageleaveand$screenleavenever count toward your plan's events. A cycle stores at most the larger of 100,000 leave events and 100 per metered event (about 100 times what views send); past it a leave is refused alone,leave_limit_reached, and nothing else of its batch is. Where your plan pauses events (Free), a batch's leave events go all or nothing: if the plan refuses any metered event of the batch, or has none left, every leave of the batch is refused with it,plan_limit_reached, even those of views that went. A malformed one (a missing or negativeengaged_ms, ascreenover 200 characters, properties over 2,048 bytes) is refused alone,validation_failed.
ravenkeep.track('$screenleave', { userId: user.id, properties: { screen: 'Lesson', engaged_ms: 42000 } })Sessions
A session is one visit. Events that carry a sessionId belong to that visit: the browser SDK (0.2.0 and later) and the Android and iOS SDKs set one on every event, and the Node SDK's track takes the browser's (getSessionId()) for a server event of the same visit. For events without one, a person's events with no gap over 30 minutes form a session. Every session needs a page or screen view: server events alone, or an SDK session of custom events only, are not a visit. A session counts on the UTC day it starts and holds events up to the end of the next UTC day.
- Duration runs from the first event to the last, so the time spent on the last page or screen counts when it is reported (
$pageleave,$screenleave); Metrics shows the median and the 75th percentile. Events sent with the publishable key are timed when they reach Ravenkeep, so a batch the browser sent late (a tab back from the background) lengthens its visit by that delay, and one held back over 30 minutes (a device asleep or offline) splits it, the 30-minute gap applying within a visit's events too. - Visitors are people: a session's visitor is the latest customer among its events, else the anonymous id it started with.
- A bounce is a session with one view that lasted under 10 seconds.
- Platform is the kind of the first view: web (
$pageview) or app ($screen). - Source and campaign are the first view's
utm_source,utm_mediumandutm_campaignproperties: the browser SDK 0.2.0 adds the address'sutm_*to$pageview, the Android SDK a deep link's to the automatic screen of the activity it opens, and the iOS SDK those of a link passed tohandleDeepLinkto the current or the next screen; otherwise, on$screen, send them yourself. A page view that lands with another campaign starts a new visit (the SDK starts a newsessionId; events without one are split by the 30-minute gap only, never by campaign). The report's filters compare each value exactly. - Entry and exit are the first and last view, by pattern, as in Pages & screens.
Features
A feature is one part of your product, defined in Metrics, then Features, for one project: a name and rules, each a page or screen pattern as Pages & screens shows it (Patient/*, /settings/billing), matched exactly or as a prefix, or an event name. A visit, by the sessions' rule, uses a feature when it viewed a page or screen, or sent an event, that a rule matches; it counts on the UTC day it starts. Owners and admins define features; everyone in the organization reads them. Nothing changes in your app: mapping screens to features is enough.
- Visitors and users that used it, and its adoption: its visitors among every visitor active in the period.
- Sessions, views and events that used it, and its engaged time: the total of its pages and screens, and the median per visitor among those with time.
- Days per visitor: the distinct days each visitor used it.
- Against the period before: the change in visitors and in engaged time, and the visitors' trend per day, week or month. The API and the MCP's
metrics_featuresalso give every figure of the period before and the trend of sessions, views and engaged time.
People across devices
Funnels and retention follow a person from before they sign in to after. A person is a contact. A device's events without a contact become that contact's when an event the report reads carries both the device's anonymous id and the contact: the browser SDK's events after a verified identify do, and so does a backend event with both userId and anonymousId. The earliest such event wins, so on a shared device the anonymous events go to the first contact who signed in on it; that contact's later events, and every other contact's, stay their own. A device never linked counts as one anonymous person. An event with neither a contact nor an anonymous id counts for nobody, and an unverified user id links nothing.
Funnels
A funnel is a path through your product, defined in Metrics, then Funnels, for one project: 2 to 10 ordered steps, a conversion window from 1 minute to 90 days, and optionally "within one session". A step is an event name, or the page or screen views of a pattern as Pages & screens shows it (/checkout/*, Patient/*), matched exactly or as a prefix, with up to 3 property filters: the event's property equals a value, compared as Metrics shows values (a string as sent, a number without trailing zeros, so 1.50 is 1.5; a missing property never matches). Of the $ names, a step takes only those Ravenkeep stores. Owners and admins define funnels, at most 50 per project; everyone in the organization reads them.
- Entered are the people with a step-1 event in the period. A person reached step 3 with events of steps 1, 2 and 3 in that order, step 3 within the window of the step 1 it continues; events of the same millisecond go by their id, and one event counts for one step. With "within one session", every step is in one session that starts in the period, by the sessions' rule; a step in a run without a page or screen view, such as a lone server event, does not count.
- Per step: the people who reached it, the conversion from step 1 and from the step before, the drop-off, and the median time from the step before (from the latest step-1 start that led to it, so step 1 on Monday and again on Friday, then step 2 a minute later, took a minute).
- Trend and breakdown: each person counts in the day, week or month of their first step-1 event of the period, converted or not; a breakdown by a property of that event shows the 10 values with the most people, the rest as other, and no value as its own group.
- People: per step, the contacts who reached it or dropped at it (reached the step before, not this one), 50 per page, each linking to their profile; anonymous people are counted, not listed. They are always of the steps the report shows: if someone changes the funnel meanwhile, the report is read again first.
- Long periods: a funnel reads at most 16 million stored events of its steps' names, plus at most 100,000 that have just arrived. Past that, the report covers the latest whole days of the period that fit and says from which day, and the trend shows no figures before it; if the last day alone holds more, the fallback store answers from the most recent 500,000 events of the period.
- Late events: an event sent with an earlier timestamp, or a sign-in that links a device on an earlier day, can take about 10 minutes to change that day's sessions and links, as in Sessions.
Retention
Metrics, then Retention groups the people of one project into cohorts by the day, week (from Monday) or month of their first event, or of their first $signup, in the history Ravenkeep keeps. A cohort is shown when it starts in the period: a period starting mid-week or mid-month starts at the next whole one, and a person seen before it is in no cohort. At most 60 cohorts: by default, days for a period of up to 60 days and weeks beyond.
- Retained in period 1, 2 and on are the cohort's people with a returning event, any event or one you choose, in the day, week or month that many after their cohort's. A period not over yet, or one running past the end of the report's period, is marked incomplete: it counts only part of its days.
- The curve is, per period, the people retained over the size of the cohorts whose period is over; where none is, the point is empty, never 0.
- Breakdown by a property of the cohort's event: each value's size and curve, the 10 largest values, the rest as other, and no value as its own group.
- Long histories: a report reads at most 650 million stored events (its history, and with a breakdown the cohorts' period twice), plus at most 100,000 that have just arrived. Past that, the fallback store reads only the most recent 500,000 events of the history, which cannot tell who is new in a cohort: the report then shows no figures and says why.
What events power
| Where | How it uses events |
|---|---|
| Metrics, Events | Hourly or daily series (UTC), totals and unique contacts for up to three events side by side, and a breakdown by one property over any period of your plan's history (up to a year). Every name appears in the event list after its first occurrence. |
| Metrics, Pages & screens | The most viewed pages ($pageview) and app screens ($screen) of any period of your plan's history, grouped by pattern, with views, visitors, unique users, the change against the period before and a trend. |
| Metrics, Sessions | Sessions, visitors, duration, views per session and bounce rate of any period of your plan's history, with a trend, the period before, and the sessions by platform, source, campaign, entry page and exit page. |
| Metrics, Features | Per feature you define (pages, screens and events of one part of your product): visitors, users, adoption, sessions, views, events, engaged time, days per visitor, with the change against the period before and a trend. |
| Metrics, Funnels | Per funnel you define (2 to 10 steps): the people who reached each step, the conversion and drop-off, the median time between steps, a trend, a breakdown by a property, and the contacts who reached or dropped at each step. |
| Metrics, Retention | Cohorts by first event or first signup per day, week or month, the share of each that returned in the periods after, the curve, and a breakdown by a property. |
| Metrics, Overview | Active users (DAU, WAU, MAU) count the contacts with at least one event that day. $revenue does not count as activity. |
| Campaign triggers | An event flow starts when a contact sends that event, optionally only when its properties match a filter (for example plan equals free). |
| Flow conditions | hasEvent branches on whether the contact sent an event within the last N days (up to 180). |
| Campaign goals | A flow's goal event counts a conversion when a contact who got one of its emails sends that event within 7 days. |
Crash grouping
The SDKs report exceptions with their stack. Ravenkeep computes a fingerprint for each occurrence on the server, and occurrences with the same fingerprint, in the same project and environment, form one crash group. The fingerprint is built in this order:
- Your fingerprint. When the report carries
fingerprint(an array of strings), it alone decides the group. - Type and top frames. Otherwise the exception type and the top 5 in-app frames, as module and function, with line and column numbers, query strings, build hashes (
index-3f9a8c.jsbecomesindex.js), absolute path prefixes and numbered directories removed. When no frame is in-app, all frames are used. - Message added. When the top frame has no function name (a minified bundle), or every frame is native, the normalized message joins the input so different bugs in the same anonymous code do not collapse into one group.
- Type and message. Without usable frames, the type and the message with numbers, ids, emails, quoted strings and URLs replaced by placeholders.
Java and Kotlin traces (the Android SDK sends the whole trace) group by their root cause: the innermost Caused by: with a frame of your app, so a framework wrapper such as Unable to start activity groups by what caused it. Line numbers, lambda and synthetic class counters and the SDK's own frames are left out, so the same bug groups alike across releases. Without an R8 mapping (not uploaded yet), an obfuscated release groups by its type, its readable frames and its message. The title stays the thrown exception's.
Swift and Objective-C stacks (the iOS SDK sends the crashing thread's frames, each with its binary image) group by their in-app functions, as image and function. Where an exception or a trap is raised (objc_exception_throw, abort, the Swift runtime's failure paths) is left out, and so are closure numbers, specializations, thunks, parameter and return types, line numbers and addresses, so the same bug groups alike across releases. A stripped release build has no names for its own frames on the device (dSYMs are not uploaded yet): it groups by its type (SIGSEGV, an NSException's name, a Swift error's type), its system frames and its message. The title is the type and the first line of the message.
// Every payment provider timeout in one group, whatever the stack
ravenkeep.captureException(error, {
userId,
fingerprint: ['payment-timeout', provider],
})Environments, releases and resolutions
- Groups are separate per
environment(lowercased,productionby default), so staging noise never hides a production crash. - Set
releaseto your version or commit. Each group records the release where it was first and last seen. - Resolve marks it fixed in the next release: it regresses only on an occurrence from a release newer than the last one it saw.
- Resolve in release X regresses on X or anything newer. Resolve now, for code without releases, regresses on any later occurrence.
- Releases compare as versions (
v1.2.10is newer thanv1.2.9, pre-releases in semver order); names that are not versions compare as different or equal. Occurrences that happened before the resolution never regress it. - A regressed group is flagged and notifies your team. Ignore keeps a group quiet for good, and Merge folds groups the fingerprint kept apart into one.
Each group shows its impact: affected customers and their share of the customers active in the last 30 days, affected accounts, the revenue at risk (the lifetime value of the affected contacts), the trend of the last 24 hours and 30 days of events. Crashes reported without a user id count as events but affect no one by name.
Support chat and feature requests
Once a signed-in customer is identified, the browser SDK shows a launcher that opens the widget. It renders in a Shadow DOM, speaks English and Brazilian Portuguese, and has two tabs.
| Tab | For your customer | For your team |
|---|---|---|
| Chat | A realtime conversation with attachments, typing indicators and read receipts. | The console Inbox (and the Android app): replies, internal notes, saved replies, assignment, priority and status, with the customer's plan, LTV, first touch and crashes beside the thread. |
| Feature requests | The requests you published, with their status, search, upvotes and a form to suggest one. | Feature requests in the console: suggestions to accept, merge or decline, statuses from under review to shipped, and each request's voters with their accounts and lifetime value. |
- Suggestions stay private until your team accepts, merges or declines them. Merging moves the customer's vote to the request it joins.
- From a conversation, your team can add the customer's vote to a request in one step.
- When a request changes status, a
featureRequestStatuscampaign flow can email its voters. - Without
identifythere is no widget: anonymous visitors cannot chat or vote.
Campaign triggers
Campaigns are email flows built in the console's visual editor: a trigger, then waits, conditions, A/B splits, emails, contact updates and webhooks. Each flow has one trigger, and the trigger decides which SDK data it needs.
| Trigger | A contact enters when |
|---|---|
event | It sends the named event, optionally only when the event's properties match a filter. Fed by track(name, { userId, properties }) from the Node SDK. |
signup | It signs up for the first time, optionally only for some attribution values (such as utmSource). Fed by signup({ userId, email, attribution }). |
featureRequestStatus | A request it voted for moves to a status (optionally one request only). Fed by votes from the widget and your team's status changes. |
manual | An operator enrolls it, picked by hand or by a filter: up to 5,000 per request and 20,000 per flow and hour. |
- Re-entry is
neverby default forevent,signupandmanualflows, andalwaysfor status changes; it can also be once every N days. A contact is never in the same flow twice at the same time. - Emails go only to contacts with a trusted email address (set with the secret key, or from a verified widget session) who did not unsubscribe; a contact whose only address came from an unverified browser session skips the email with the reason
untrusted_email. Every email carries an unsubscribe link and theList-Unsubscribeheader, in the contact'slocaletrait or the project's default language; opens and clicks are tracked per step. - Published flows are versioned: contacts already in a flow finish the version they started.
- Campaign emails go out through your own email provider, from your own address: connect your Resend account with an API key in the project's Email sending settings (Resend's free plan sends up to 3,000 emails a month, 100 a day). You verify your domains in Resend, and Ravenkeep stores the key encrypted. Until a provider is connected, a flow that sends email cannot be published, and Ravenkeep's own address only sends test emails, each to the person on your team who asks for it, once their sign-in address is verified.
Limits and rate limits
Past a rate limit the API answers 429 rate_limited with a Retry-After header; the SDKs wait and retry their batches. Each route family (events, signups, crashes, widget) has its own budget, so a burst of events never delays a signup. The limits are per project unless the table says otherwise.
| Rate | Limit |
|---|---|
| Events | Secret key, POST /v1/events and GET /v1/ping: 200 requests per second, bursts of 400 |
| Signups | Secret key, POST /v1/signup, /v1/identify and /v1/revenue together: 100 requests per second, bursts of 200 |
| Crashes | Secret key, POST /v1/crashes: 100 requests per second, bursts of 200. Crashes themselves: 2,000 per second with the secret key and 400 per second with the publishable key; from browsers, at most 100 new crash groups per hour |
| Publishable key | Per route family: 20 requests per second per visitor IP address (bursts of 40), and 200 per second for the whole project (bursts of 400) |
| Widget | Messages, votes, suggestions and uploads: 10 per minute each, per contact |
| Invalid keys | After 60 failed key lookups in a minute from one address, answers become 429 |
| Payload | Limit |
|---|---|
| Request body | 1 MiB |
| Batch | 500 events, or 50 crashes |
| Event | Name up to 100 characters, properties up to 32 KiB (2,048 bytes for $pageleave and $screenleave), messageId deduplicated for 7 days |
| Event names | 1,000 distinct names per project; a new name past it is refused with event_name_limit |
| Event time (secret key) | Up to 366 days in the past (or the plan’s history, when shorter) and 1 day ahead; publishable-key events are stamped with the time they arrive |
| Contact | Ids up to 255 characters, traits up to 32 KiB per call and 64 KiB in total |
| Crash | Stack up to 256 KiB, 250 frames, 50 breadcrumbs, 50 tags, context up to 64 KiB |
| Revenue | Up to 10^12 minor units per record |
| Attachments | 10 MB each, 10 per message, and 2 GiB per project in any 24 hours |
| Allowed origins | 50 per project |
| Apps | 50 per project |
Data retention
Ravenkeep keeps what reports need and compacts the rest on a daily schedule. Deleting a project deletes its data.
| Data | Kept |
|---|---|
| Contacts | Profiles, first-touch attribution and lifetime value, until the project is deleted. Attribution is never overwritten. |
| Raw events | 366 days, or the plan’s history when shorter. Older events are dropped once their daily counts exist; the daily counts stay. |
| Event counts | Hourly counts for 35 days; daily counts and unique contacts for good. |
| Active contacts | Per contact and day for 400 days, then as daily totals (DAU, WAU, MAU) for good. |
| Crash occurrences | Every occurrence of the last 30 days, and the newest 100 of each group beyond. Group counters and history stay. |
| Support | Conversations, messages, feature requests and votes, until they or the project are deleted. Attachments never sent in a message are removed after 24 hours. |
| Campaign runs | Step logs for 90 days after a run ends, finished runs and individual clicks for 180 days. Per-step counters stay. |
| Notifications | Your team's read notifications for 90 days, and at most the newest 2,000 per person in each organization. |
| Deduplication ids | 7 days. |