SDK Reference
Plan and revenue
Record plan changes and revenue from your billing code with sendLifecycleEvent.
Otis follows each paying account through conversion, upgrades, downgrades and cancellation. When you also send what each account pays, Otis can report revenue figures such as monthly recurring revenue and net revenue retention. Lifecycle describes the stages and movements Otis records.
You can tell Otis about plans in two ways, and most products use both. The first keeps a plan property current on each account. The second records each change from your billing code at the moment it happens.
Keep the plan property current
The basic method is a plan property on the account, set with setGroupProperties(), or with setUserProperties() for a product sold to individuals. Otis records a plan change when the property's value changes. Identity and properties covers the property, and Revenue amounts covers sending what the account pays.
A property change has two limits. Otis dates the change to the day it sees the new value, and your app only sends the new value when it next sets the property. For an account that isn't using your product around the change, that can be days later. A change that reverses before your app next sets the property is never recorded.
Record the change from your billing code
Call sendLifecycleEvent() in the code that changes the plan, such as your billing webhook handler or checkout success handler. Otis then records the change on the date it happened. While an account has an event from the last two days, Otis takes the account's plan from the event and not from the property. After that, Otis reads the property again, so keep the property current as well.
import { sendLifecycleEvent } from "@runotis/sdk";
// In the billing webhook handler, after applying the subscription change:
sendLifecycleEvent({
groupType: "account",
groupId: acmeAccountId,
previousPlan: "trial",
plan: "pro",
});Required fields:
plan, the plan after the change. Use the same values your app sends as theplanproperty. Otis ignores case and surrounding spaces when it compares them.- The account. Pass
groupTypeandgroupIdfor the paying account. The SDK hashes the group ID the same way it hashes the groups you pass toidentifyUser(), so the event joins to the same account. For a product sold to individuals, passuserIdinstead.
The SDK throws if plan is empty, if you pass neither an account nor a userId, or if you pass only one of groupType and groupId.
Optional fields:
previousPlan, the plan before the change. Leave it out when an account receives its first plan.mrr_minor,previous_mrr_minorandcurrency, what the account pays after and before the change. See Send the amounts.- Any other key, stored on the event with a
lifecycle.prefix, soseatsbecomeslifecycle.seats. Otis doesn't scan these values for personal data unless you mark the key.
Send the amounts
If your billing code knows what the account pays as well as its plan, send both sides of the change:
sendLifecycleEvent({
groupType: "account",
groupId: acmeAccountId,
previousPlan: "pro",
plan: "enterprise",
previous_mrr_minor: 49900, // $499/month before
mrr_minor: 249900, // $2,499/month after
currency: "USD",
});mrr_minor is monthly recurring revenue (MRR) in minor units, such as cents for US dollars. The amounts follow the same rules as the property form: integer minor units, normalized to a month. previous_mrr_minor is optional. When you leave it out, Otis uses the last amount it recorded for the account.
These amounts are the most accurate revenue information you can send from your code. Your handler knows the amount before and after at the moment of the change. A property update shows Otis only the current amount, so Otis has to compare it with an earlier value to find the change.
Keep setting mrr_minor on the account as well. The event records a change, and the property records the current amount. Otis needs both.
How Otis classifies a change
Send the plan values and let Otis decide what kind of change happened, because there is no field for "upgrade" or "conversion". Otis compares the new plan with the plan it already holds for the account, using the order of your plans, which your team gives Otis during onboarding. It uses previousPlan only when that agrees with its own record, or when it holds no plan for the account yet. It then labels the change as a trial start, conversion, upgrade, downgrade, plan change or cancellation. Without a plan order, Otis guesses from each plan's name whether it is free, trial or paid, and can't tell whether a move between two paid plans went up or down.
Because the classification comes from the plan order, a caller can't mislabel an upgrade as a conversion. Update the plan order before you rename a plan or launch a new one. Otis records a rename as a plan change for every account on that plan, and it treats a plan name it can't place as not paying.
Send the event from your server
Call sendLifecycleEvent() only where the plan actually changes, such as your billing webhook or checkout success handler. Don't call it from the browser or on page load. Webhook handlers run outside any user's request, which is why the event names the account explicitly. Keep setting the plan property as well, because the event adds to the property rather than replacing it.