Skip to content

@knowify/sdk — Official TypeScript SDK ​

The fastest way to call the Knowify Developers API from TypeScript or JavaScript. ESM-first, dual ESM/CJS exports, browser-compatible, Node 20+.

bash
npm install @knowify/sdk
ts
import { Knowify } from '@knowify/sdk';

const k = new Knowify({ accessToken: process.env.KNOWIFY_TOKEN! });

const open = await k.projects.list({
  where: { BusinessState: 'Open' },
  limit: 25,
});

for await (const p of k.projects.iter({ where: { OutstandingAmount: { $gt: 0 } } })) {
  console.log(p.Id, p.ProjectName, p.OutstandingAmount);
}

Canonical docs live in the README

The full reference, with every option, every error class, every recipe, lives in the SDK README on GitHub. This page is your jumping-off point — the deep links below land you on the exact chapter you need.

Why an SDK over raw HTTP? ​

The v2 REST API is fully self-describing — you can hit it with curl or any HTTP client. But the SDK gives you:

  • One typed import for every resource — import { Knowify, ProjectOutput } from '@knowify/sdk'.
  • Class + namespace ergonomics — k.projects.list(...), k.invoices.create(...). No threading a client argument anywhere.
  • Typed error subclasses — if (e instanceof NotFoundError) e.resource instead of unpacking RFC 7807 problem documents by hand.
  • Lazy multi-page iteration — for await (const p of k.projects.iter()) fetches one page at a time, no upfront row count.
  • Composable middleware — opt-in retry, logging, observability, timeout; per-request overrides; bring your own (refresh-token, caching, …).
  • AbortSignal everywhere — cancellation flows through the whole stack.

Authentication ​

The SDK supports bring-your-own-token AND ships helpers to obtain tokens. Full surface in the README; quick tour here.

Bring your own token ​

ts
new Knowify({ accessToken: '...' });   // OAuth 2.0 bearer
new Knowify({ kauthToken:  '...' });   // direct kAuth — admin scope

OAuth tokens carry only the scopes the client was granted — calls needing a missing scope throw InsufficientScopeError. Direct kAuth grants the admin meta-scope. For external integrations, use OAuth with narrow scopes.

Username + password login ​

ts
const result = await Knowify.login({
  username: 'user@example.com',
  password: 'secret',
});

if (result.status === 'authenticated') {
  const k = result.client;
} else {
  // Advisor with multi-tenant access
  const k = await result.assumeTenant(result.tenants[0].Id);
}

Returns a discriminated union — the 'choose_tenant' branch is for advisor accounts. Hits POST /api/v2/auth/login (and /auth/advisor-assume for the advisor flow).

OAuth flows ​

For client credentials (server-to-server), device code (CLI), refresh, revoke, introspect:

ts
import { KnowifyOAuth } from '@knowify/sdk';

const oauth = new KnowifyOAuth({ clientId: 'my-app' });

// Server-to-server
const tokens = await oauth.clientCredentials({ clientSecret, scopes: ['projects:read'] });

// CLI / device code
const flow = await oauth.startDeviceCode({ scopes: ['projects:read'] });
console.log(`Visit ${flow.verificationUriComplete} — code ${flow.userCode}`);
const tokens = await oauth.pollDeviceCode(flow);

const k = new Knowify({ accessToken: tokens.accessToken });

All OAuth endpoints are discovered lazily via OIDC /.well-known/openid-configuration — no hardcoded paths. Authorization Code with PKCE is intentionally not yet exposed (no consumer use case in scope today); see the OAuth flows guide for the raw HTTP shape if you need it now.

Auto-refresh + rolling tokens ​

Two middlewares handle token lifecycle transparently:

ts
import { autoRefresh, kAuthRollingToken } from '@knowify/sdk';

// OAuth: catch 401s, refresh, retry — single-flight, fires onTokensRefreshed for persistence.
k.use(autoRefresh(k, {
  refreshToken,
  clientId: 'my-app',
  onTokensRefreshed: (newTokens) => storage.save(newTokens),
}));

// kAuth: watch the rolling `kAuth` response header, update client + persist on rotation.
k.use(kAuthRollingToken(k, {
  onTokenRefreshed: (newToken) => storage.save(newToken),
}));

Both update the in-flight client via k.setAccessToken(...) / k.setKauthToken(...) before firing the consumer callback — so by the time you're persisting, the client is already using the new token.

The client and resources ​

Knowify is a single class. Construct it once, share it across your app. Every resource hangs off it as a lazy namespace — accessors instantiate on first read so unused resources cost nothing.

ts
k.projects     k.invoices       k.clients      k.vendors
k.contracts    k.milestones     k.tasks        k.allocations
k.purchases    k.purchaseItems  k.items        k.payments
k.documents    k.listItems      k.bills        k.departments
k.resources    k.submittals     k.assets       k.aiaInvoices
k.serviceTickets   k.timeEntries   k.billables   k.users

Each resource exposes the canonical CRUD surface — list, query, iter, get, create, update, delete — with some resources adding custom endpoints alongside. The resource list in the README maps every accessor to its endpoint family.

Pagination ​

ts
// Single page — when you want meta.total or a paged UI
const page = await k.projects.list({ limit: 50 });

// Lazy iteration — when you want to stream every match
for await (const p of k.projects.iter({ where: { BusinessState: 'Open' } })) {
  // …
}

iter() fetches the next page only when you read past the current one. Breaking out of the loop stops pagination immediately — no extra requests. Full pagination docs →

Errors ​

Every API error throws a typed KnowifyError subclass. Branch on instanceof — don't string-switch on code.

ts
import { NotFoundError, ValidationError, InsufficientScopeError, RateLimitedError } from '@knowify/sdk';

try {
  await k.projects.get(99999);
} catch (e) {
  if (e instanceof NotFoundError)         // e.resource, e.resourceId
  else if (e instanceof ValidationError)  // e.issues
  else if (e instanceof RateLimitedError) // e.retryAfter
  else if (e instanceof InsufficientScopeError) // e.required, e.granted
}

The full error code vocabulary and subclass reference covers all 11 codes, the extension fields on each, and the agent-readable toString() format.

The HTTP/RFC 7807 shape is documented at Responses & Errors.

Middleware ​

The SDK uses a Hono-style onion model for composable behavior. Six built-in middlewares ship; custom middlewares slot into the same chain.

ts
import { Knowify, retry, logging, timeout, observability, autoRefresh, kAuthRollingToken } from '@knowify/sdk';

const k = new Knowify({ accessToken })
  .use(logging({ logger: pino() }))
  .use(retry({ maxAttempts: 3 }))
  .use(timeout(30_000))
  .use(observability({
    onResponse: (req, res, ms) => metrics.observe(req.method, res.status, ms),
  }))
  .use(autoRefresh(k, { refreshToken, clientId, onTokensRefreshed: save }));
Built-inPurpose
retryExponential backoff + jitter on 429/5xx + transport errors. Honors Retry-After.
loggingStructured request/response logs (Pino-compatible).
observabilityOpinion-free onRequest / onResponse / onError hooks.
timeoutPer-request timeout via AbortSignal.timeout().
autoRefreshOAuth — catch 401, refresh, retry (single-flight).
kAuthRollingTokenkAuth — watch rolling kAuth response header, update client.

Order matters — the first .use() is OUTERMOST in the onion (sees the request first, sees the response last). Full middleware docs → including the contract, per-call middleware, introspection under k.middleware, and worked examples (refresh-token handling, OpenTelemetry tracing, etc.).

Cancellation ​

Native AbortSignal everywhere — pass one to a call, set a default on the client, or compose both:

ts
// Per-call
await k.projects.list({}, { signal: AbortSignal.timeout(5_000) });

// Client default
const k = new Knowify({ accessToken, signal: AbortSignal.timeout(30_000) });

Testing ​

The SDK is friendly to your existing test tooling — vitest, jest, msw, nock. We ship one helper, mockError, for building realistic RFC 7807 problem payloads to use with whichever mocker you prefer. Full testing patterns →

ts
import { vi } from 'vitest';
import { Knowify, NotFoundError } from '@knowify/sdk';
import { mockError } from '@knowify/sdk/testing';

const k = new Knowify({ accessToken: 'test' });

vi.spyOn(k.projects, 'get').mockRejectedValue(
  new NotFoundError(mockError({
    status: 404, code: 'not_found', resource: 'project', resourceId: 999,
  }).body as any, new Response()),
);

What else lives in the README ​

Source + issues ​

See also ​