SDK Reference
@kaquill/node — server-side, dependency-free, Node 18+. Four methods. New here? Start with the Quick Start.
Constructor
import { Kaquill } from "@kaquill/node";
const kq = new Kaquill({
apiKey: process.env.KAQUILL_API_KEY!,
timeoutMs: 800,
onError: (err) => logger.warn({ err }, "kaquill"),
});| Option | Type | Notes |
|---|---|---|
| apiKey | string, required | Your workspace key. Starts with kq_. Server-side only. |
| host | string | Defaults to https://api.kaquill.com. |
| timeoutMs | number | Defaults to 800. Past this the call fails open and returns null. |
| defaultUserId | string | Used when a call is made without a user id. Empty strings count as missing, so `user?.id ?? ""` falls back here. |
| onError | (err: Error) => void | Non-fatal errors. Defaults to console.error outside production. |
| fetch | typeof fetch | Inject your own fetch, for tests or a proxy. |
identify(userId, traits)
Upserts traits. No decision comes back — it is a profile write. Traits are optional and free-form beyond the documented keys; undefined values are stripped, while null, 0 and false are kept.
await kq.identify("usr_8812", {
plan: "trial",
trial_started_at: "2026-09-18T10:00:00Z",
trial_ends_at: "2026-10-02T10:00:00Z",
email: "ada@example.com",
company: "Example Ltd",
team_size: 12,
});
// -> Promise<void> (204 from the server)Documented traits: plan, trial_started_at, trial_ends_at, email, company, team_size.
track(userId, event, properties?)
Records a behavioural event and returns the decision it triggered, if any.
const result = await kq.track("usr_8812", "pricing_viewed", {
plan_viewed: "growth",
});
result.ok // false if the call failed - it never throws
result.decision // Decision | nullconvertedToPaid(userId, conversion)
Sends a converted_to_paid event carrying the plan value. discountApplied is what makes the counterfactual answerable later — send it even when it is false.
await kq.convertedToPaid("usr_8812", {
plan: "growth",
mrr: 99,
discountApplied: true,
metadata: { coupon: "LAUNCH20" },
});
// -> Promise<void>decide(userId)
Asks for a decision without an event, for questions like “what should this trial user see on the pricing page?”
const decision = await kq.decide("usr_8812");
// -> Decision | nullThe Decision object
Returned verbatim from the API. DoNothing is never a decision — you get null instead.
interface Decision {
action_name: string; // OfferDiscount20 | OfferTrialExtension
// | ShowUpgradeCTA | ShowFeatureNudge
// | SendSlackAlert
action_id: number; // 1-5
confidence: number; // 0.0-1.0
logic_path: string; // e.g. "AI_TREATMENT"
copy?: {
headline: string | null;
body: string | null;
cta: string | null;
source: string;
};
}The outbound webhook envelope describes the same decision with different field names (action, headline, body). If you consume both, see Webhooks & delivery.
Failure behaviour
- Nothing throws into your code path. Failures resolve to
{ ok: false, decision: null }ornull. - Requests abort at
timeoutMs(800 by default), so a slow KaQuill cannot slow your app down. - A missing user id is reported through
onErrorand the call is skipped rather than sent.