Quick Start
Four calls, all server-side. You send what your trial users do; KaQuill answers with whether to intervene, and only for the users where it would change the outcome.
There is no browser SDK and no snippet to paste into your front end. KaQuill never renders anything and never sends email on your behalf.
1. Get your API key
Dashboard → Settings → API. Keys start with kq_. Treat it as a server secret: it authenticates as your whole workspace, so it does not belong in client-side code.
2. Install
npm install @kaquill/nodeNode 18 or newer. The SDK has no dependencies and uses the built-in fetch.
import { Kaquill } from "@kaquill/node";
export const kq = new Kaquill({
apiKey: process.env.KAQUILL_API_KEY!, // kq_live_... from Settings -> API
});Options you may want: timeoutMs (default 800), defaultUserId, onError to pipe non-fatal errors into your own logger, and host if you are not on api.kaquill.com. Full list in the SDK Reference.
3. Identify your trial users
Traits are what the model reasons about before a user has done anything. trial_ends_at is the single most useful one: it is what lets the policy tell a user with nine days left from one with nine hours.
// When a trial starts, or whenever these traits change.
await kq.identify(user.id, {
plan: "trial",
trial_started_at: user.trialStartedAt, // ISO 8601
trial_ends_at: user.trialEndsAt, // ISO 8601 - drives urgency
company: user.company,
team_size: user.teamSize,
});4. Track what they do
Send the events that carry intent — hitting a limit, viewing pricing, inviting a teammate, a second failed export. Every call returns a decision or null.
// Anywhere a trial user does something that matters.
const { decision } = await kq.track(user.id, "export_blocked", {
limit: 5,
});
if (decision) {
// decision.action_name -> "OfferDiscount20" | "OfferTrialExtension"
// | "ShowUpgradeCTA" | "ShowFeatureNudge"
// | "SendSlackAlert"
// decision.confidence -> 0.0-1.0
// decision.copy -> { headline, body, cta, source } when generated
render(decision);
}null.5. Tell it what happened
Conversions close the loop, and discountApplied is the field the whole counterfactual rests on. Without it, nothing can learn whether a discount was needed.
await kq.convertedToPaid(user.id, {
plan: "growth",
mrr: 99,
discountApplied: true, // the counterfactual input - always send it
});Two things that will otherwise confuse you
Your workspace starts in shadow mode
New workspaces are created with a rollout of zero: decisions are computed and logged, but nothing is returned to fire. That is deliberate — it lets you integrate and inspect real decisions before any user sees one. Ask us to switch you on when you are ready.
Ten percent of your users are a holdout
Every workspace gets a sticky randomised control group. Those users are assigned once and never intervened with, which is what makes it possible to report lift with a confidence interval rather than a number you have to take on trust. Their decisions are still recorded as what KaQuill would have done.
Without the SDK
The SDK is four HTTP calls. If you are not on Node, post to the API directly.
curl -X POST https://api.kaquill.com/v1/events \
-H "Authorization: Bearer $KAQUILL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "usr_8812",
"event_name": "export_blocked",
"properties": { "limit": 5 },
"timestamp": "2026-09-25T09:30:00Z"
}'timestamp on every request. The SDK always does. A hand-rolled client that omits it currently fails event normalisation and gets a 400 back.A decision comes back like this:
{
"decision": {
"action_name": "ShowUpgradeCTA",
"action_id": 3,
"confidence": 0.81,
"logic_path": "RULE_PRICING_INTENT_UPGRADE"
}
}When there is nothing to do, the response is { "decision": null }.
If KaQuill is down
The SDK fails open. Timeouts, network errors and bad keys never throw into your code path — calls resolve with { ok: false, decision: null } and the error goes to your onError handler. Your trial flow keeps working whatever we are doing.
Next
- SDK Reference — every method, option and payload.
- Webhooks & delivery — get the same decision pushed to your own endpoint or Slack.