# @stowem/sdk

> Thin zero-dependency TypeScript SDK for the Stowem structured data-capture API.

Latest version **0.1.2** (published 2026-09-24) · MIT license · 0 weekly downloads

## Install

```sh
npm install @stowem/sdk
pnpm add @stowem/sdk
yarn add @stowem/sdk
bun add @stowem/sdk
```

## Health

**Score 70/100 (B)** — status: active.

Positive: has types; esm support; no vulnerabilities; recently updated; high maintenance score; high quality score.

Warnings: low downloads; pre 1.0.

## Facts

| | |
|---|---|
| Version | 0.1.2 |
| Published | 2026-09-24 |
| First published | 2026-09-24 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Node | >=18 |
| Dependencies | 0 |
| Unpacked size | 130.3 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Author | Wise Automation Solutions Inc |
| Maintainers | wise-automation-solutions |
| Keywords | stowem, structured-data, extraction, llm, data-capture, sdk |

## Links

- npm: https://www.npmjs.com/package/@stowem/sdk
- Homepage: https://github.com/Wise-Automation-Solutions/stowem#readme
- Issues: https://github.com/Wise-Automation-Solutions/stowem/issues
- npm.io page: https://npm.io/package/@stowem/sdk

## Recent versions

- 0.1.2 (latest) — 2026-09-24
- 0.1.1 — 2026-09-24
- 0.1.0 — 2026-09-24

## README

# @stowem/sdk

Thin, zero-dependency TypeScript wrapper for the Stowem structured data-capture API. Node 18+, Deno, Bun, and edge runtimes. Native `fetch` only.

- **Start here:** a runnable example, [`examples/clinic-intake`](https://github.com/Wise-Automation-Solutions/stowem/tree/main/examples/clinic-intake)
- **[API reference](https://github.com/Wise-Automation-Solutions/stowem/blob/main/docs/api-reference.md)** — the request, the response, reconciliation, pricing, limits
- **[Errors](https://github.com/Wise-Automation-Solutions/stowem/blob/main/docs/errors.md)** — every code, what it costs, and what to do

## Install

```sh
npm install @stowem/sdk
```

## Setup

```ts
import { Stowem } from '@stowem/sdk';

const stowem = new Stowem({
  apiKey: process.env.STOWEM_API_KEY!, // required; throw on boot if unset
  routes: {
    update_patient_fields: {
      schema_resource: 'patient',
      method: 'PATCH',
      path: '/api/patients/{patient_id}',
      body: {
        dateOfBirth: '<set.date_of_birth>',
        condition: '<set.ongoing_conditions>',
      },
    },
    add_patient_medication: {
      schema_resource: 'patient',
      method: 'POST',
      path: '/api/patients/{patient_id}/medications',
      body: { items: '<add_to_array.medications>' },
    },
  },
});
```

Route URLs, methods, and body templates stay on your server. Only the capability declaration (`id`, `binds_to`, `accepts_changes`) travels to Stowem — and you never write it: it is derived from `schema_resource` plus the placeholders in your body template, so the two halves cannot drift. `stowem.declaredRoutes(schema)` returns exactly what will be sent.

A change with no placeholder to fill would vanish from the request body without a trace, so `resolve()` refuses it (`unfillable_change`) rather than sending a body that looks right.

### Clearing fields

A plan carries only the *names* of the fields being cleared, so there are two placeholder forms:

```ts
// PATCH-shaped: fills with null when cleared, omitted from the body when not.
body: { emergencyContactName: '<unset.emergency_contact_name>' }

// List-shaped: fills with every field being cleared.
body: { clear_fields: '<unset>' }
```

## One call

```ts
import { randomUUID } from 'node:crypto';

const result = await stowem.plan({
  inputs: [
    { type: 'exchange', data: conversationTurns },
    { type: 'document', data: extractedPdfText },
  ],
  schema: patientSchema,
  saved_state: { patient: currentPatientRecord }, // keyed by resource, like the schema
  path_params: { patient_id: 'patient:789' }, // SDK-only; never sent to Stowem
  idempotency_key: randomUUID(), // one per save attempt; reuse it only to retry that attempt
});
```

No `routes` argument: the declaration comes from the config above. To cancel a call — a closed chat window, a user who navigated away — pass a signal:

```ts
const result = await stowem.plan({ ... }, { signal: controller.signal });
```

The SDK also aborts on its own at 65 s, just past the server's 60 s cap, so the server's `timeout` envelope normally arrives first.

## One resolve

```ts
if (result.status === 'plan') {
  const requests = stowem.resolve(result); // deterministic, local, no network

  await Promise.allSettled(
    requests.map((req) =>
      // req.url is a path ("/api/patients/patient:789"); prefix your backend's origin.
      fetch(yourBackend + req.url, {
        method: req.method,
        headers: { Authorization: `Bearer ${authToken}`, 'Content-Type': 'application/json' },
        body: JSON.stringify(req.body),
      }),
    ),
  );
}
```

`resolve()` reads `path_params` from the result object (`plan()` attaches them as `result.pathParams`), so if you store a plan to resolve later, keep that property with it.

`resolve()` throws before building any URL on: `unknown_route`, `mixed_verbs`, `duplicate_set`, `unsafe_reference`, `missing_path_param` (a URL placeholder with no value), `invalid_route_config`, and `unfillable_change` (a change your body template has no placeholder for).

## Conflicts and updates

`conflicts` is for values two sources disagreed about — worth a human's attention. A value that simply differs from what you passed in `saved_state`, with no disagreement, arrives in `changes_to_saved` instead:

```ts
if (result.status === 'plan') {
  for (const c of result.conflicts) {
    // Two sources disagreed. Consider asking the user.
  }
  for (const u of result.changes_to_saved) {
    // An ordinary update: u.saved_value -> u.new_value. Log it; don't prompt.
  }
}
```

## What the extraction saw

Beside the receipt, every response carries an `extraction` object — about the extraction rather than the bill:

```ts
console.log(result.extraction);
// {
//   tabular_rows: 500,
//   invalid_records: 0,
//   dropped_changes: [],
//   unusable_values: [],
//   overridden_unsets: [],
// }
```

**`dropped_changes` is the one to check first when a plan looks thin.** It lists values we read and then discarded because no route declared that `(schema_resource, verb, field)` — almost always because a `string[]` field was declared under `set`, when list items are only ever `add_to_array`. An empty array is a correct configuration.

`unusable_values` is the other half of the same question: values we read and understood but could not store, because they did not fit the field you declared — an answer outside an `enum`'s option list, a date that would not resolve. The value we saw is echoed back, because which value it was is the whole diagnostic: an `enum_no_match` is usually an option list too short for what your users actually say, not a model mistake.

`overridden_unsets` lists fields the user asked to clear and then gave a new value for in the same request. The value wins and the clear is discarded — usually because they changed their mind mid-sentence, which is exactly right. Worth a look when it is frequent, or when the clear was the part they meant: `unset` is the verb worth confirming before you execute it.

`invalid_records` above `0` means we dropped records we could not trust: the plan is a *partial* answer, and what is in it is still correct. `tabular_rows` is the data rows we counted in your `tabular` inputs — compare it with the items in the plan, because a very large list can come back partly extracted and this is the only way to see it. It counts `tabular` inputs only; a long list inside a `document` has no row count we can take without extracting it first.

## What each call cost

Every successful response carries an itemized `usage` receipt. All money is an integer in **micro-USD** — millionths of a dollar — because a typical charge is well under a cent and a float would drift.

```ts
const result = await stowem.plan({ ... });

console.log(result.usage);
// {
//   input_tokens: 1866,
//   output_tokens: 212,
//   input_charge_micro_usd: 2799,
//   output_charge_micro_usd: 1590,
//   base_fee_micro_usd: 1500,
//   total_charge_micro_usd: 5889,   // $0.005889
//   balance_micro_usd: 24354111,    // $24.354111 left
//   price_version: '2026-09-18',
// }

const dollars = (micro: number) => (micro / 1_000_000).toFixed(6);
```

Tokens are **Stowem tokens**, `ceil(characters / 4)` over the request body you sent and the response body you got back (excluding the receipt itself) — not a model tokenizer. Our model choice, routing and retries never change your bill. The counting rule and the rate card are in the [API reference](https://github.com/Wise-Automation-Solutions/stowem/blob/main/docs/api-reference.md#pricing), so you can check any receipt against your own payload size.

## Errors

`plan()` throws `StowemAPIError` on any non-2xx, carrying the HTTP status and the wire error envelope. It also throws it on a 2xx whose body is not JSON, and on a write failure sending a body over the 2 MB platform cap (which the server refuses before the upload finishes, so no envelope comes back) — so every answer the API gives arrives as `StowemAPIError`. **A network failure or a timeout is different:** it arrives as the underlying `fetch` error — a `TypeError` when there is no connection, an `AbortError` when the SDK gives up at 65 s or your own `signal` fires. After one of those you cannot know whether the request ran, so retry with the same `idempotency_key`. `resolve()` never touches the network and throws `StowemResolveError` instead. Every code is in [errors.md](https://github.com/Wise-Automation-Solutions/stowem/blob/main/docs/errors.md).

```ts
import { StowemAPIError } from '@stowem/sdk';

try {
  const result = await stowem.plan({ ... });
} catch (err) {
  if (err instanceof StowemAPIError) {
    switch (err.envelope.error) {
      case 'insufficient_credits':   // 402 — top up; no model call was made, so this was free
      case 'spend_cap_exceeded':     // 402 — your own spend cap, not your balance
        return alertBilling(err.envelope.message);
      case 'account_suspended':      // 403 — valid key, suspended account; rotating won't help
        return alertOps(err.envelope.message);
      case 'idempotency_in_progress': // 409 — an earlier attempt is still running; retry the same key
      case 'rate_limited':            // 429 — per account, not per key
      case 'service_paused':          // 503 — our protective stop, not you
      case 'timeout':                 // 504 — charged (input + base fee); a retry is charged again
        return retryLater(err);
      default:
        throw err;                    // 400-class: a bug in the request
    }
  }
  throw err;
}
```

Every rejection that happens before a model call is free — including `402`, `403`, `409`, `429` and `503`.

## Just the template filler

```ts
import { fillTemplate } from '@stowem/sdk/template';

const body = fillTemplate(route.body, operation.changes);
```

Object traversal, not string substitution — your `JSON.stringify` does the escaping, so extracted values can never break the wire payload.

## API reference

The full reference — request shape, input types, reconciliation rules, limits, pricing — is [docs/api-reference.md](https://github.com/Wise-Automation-Solutions/stowem/blob/main/docs/api-reference.md); every error code is in [docs/errors.md](https://github.com/Wise-Automation-Solutions/stowem/blob/main/docs/errors.md).

---
_Source: https://npm.io/package/@stowem/sdk · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
