TypeScript SDK
@incantory/sdk: resolve owner/slug@label refs to hash-verified versions, render with typed variables, pin in incantory.lock and report evals.
@incantory/sdk is built on fetch, so it runs on Node 20.19 or later, in browsers, on edge runtimes and in Deno. It ships ESM and CommonJS, and its only runtime dependency is @incantory/shared (the same render and hashing code the website uses).
npm install @incantory/sdkimport { Incantory } from '@incantory/sdk';
const inc = new Incantory({ token: process.env.INCANTORY_TOKEN }); // token optional for public prompts
const prompt = await inc.prompts.get('alice/code-review@production');
const { text } = prompt.render({ language: 'Go', code }) as { text: string };Client options
new Incantory({
token, // inc_... API token; default $INCANTORY_TOKEN (Node). '' forces anonymous
baseUrl, // default $INCANTORY_URL or https://incantory.ai
cacheDir, // disk cache (Node); false = memory only; default $XDG_CACHE_HOME/incantory or ~/.cache/incantory
cache, // bring your own CacheStore
fetch, // custom fetch (tests, proxies)
lockfile, // path to incantory.lock: refs in it resolve to their pinned version
maxRetries, // default 3
timeoutMs, // default 30000
userAgent, // appended to X-Incantory-Client
});The constructor does no I/O; the cache and lockfile are opened on first use.
Resolving prompts
inc.prompts.get(ref, { offline?, ignoreLock? }) returns a Prompt.
| Ref | What happens |
|---|---|
owner/slug@v3 | Served from the local cache with no request once cached; otherwise GET /versions/3 (immutable) |
owner/slug@production | GET /labels with If-None-Match (usually a 304), then the version as above |
owner/slug | GET /prompts/{owner}/{slug} with ETag revalidation; the latest version is embedded |
Every downloaded version is re-hashed with incantory-tree-v1; a mismatch with the server's contentHash throws integrity_error.
If the API cannot be reached, label and latest lookups fall back to the last cached answer and set prompt.stale = true. Pass { offline: true } to skip the network entirely (offline_cache_miss when nothing is cached).
The Prompt value
| Field or method | Contents |
|---|---|
ref / requestedRef | The pinned ref (owner/slug@v3) and the ref you asked for |
version, contentHash, kind | Version number, hash and prompt kind |
files, frontmatter, variables, targetModels | Package contents and metadata |
yanked, deprecation | Whether the version was yanked, and the Deprecation header |
fromCache, stale | Where the answer came from |
render(vars, { onMissing }) | { kind: 'text', text } or { kind: 'chat', messages } |
text(vars) | The rendered output as plain text |
toFiles() | Files as they would be written to disk |
verify() | Recompute and check the hash |
Chat prompts render to messages, split on ## system / ## user / ## assistant before variables are substituted.
Registry calls
| Method | Endpoint |
|---|---|
prompts.detail(ref) | GET /prompts/{o}/{s} with ?version or ?label |
prompts.list(q), prompts.search(text, q), prompts.iterate(q) | GET /prompts (keyset cursors) |
prompts.versions(ref, { cursor, limit }) | GET /versions |
prompts.version(ref, n) | GET /versions/{n} |
prompts.diff(ref, a, b) | GET /diff/{a}..{b} |
prompts.labels.list / set / delete / history | /labels (set can fail with version_yanked or eval_required) |
prompts.fork(ref, { version, slug, licenseSpdx }) | POST /forks |
prompts.create(input) | POST /prompts |
prompts.commit(ref, { files, message, baseVersion }) | POST /versions (409 stale_base / no_changes) |
me() | GET /api/me |
Pinning with incantory.lock
await inc.lock.pin(['alice/haiku@production', 'bob/review']); // resolve now and write the pins
const results = await inc.lock.verify(); // [{ ref, version, status: ok|mismatch|missing|error, outdated, latestVersion }]
const pinned = new Incantory({ lockfile: 'incantory.lock' }); // get() now serves pinned versions{
"lockfileVersion": 1,
"prompts": {
"alice/haiku@production": { "version": 3, "contentHash": "...", "resolvedAt": "2026-09-28T12:00:00.000Z" }
}
}The file is byte-identical whether the TypeScript SDK, the Python SDK or the CLI writes it. A client configured with a lockfile throws lock_mismatch if the registry ever serves different bytes for a pinned version.
Typed variables
npx incantory codegen alice/haiku@production -o src/prompts.d.tsThe generated file augments PromptVariablesRegistry, so prompts.get('alice/haiku@production') returns a prompt whose render() arguments are typed: required variables are enforced, enums become literal unions, and a mistake is a compile error. generateTypes({ owner, slug, package }) does the same programmatically.
Reporting evals
import { parseJsonl } from '@incantory/sdk';
const results = parseJsonl(await readFile('results.jsonl', 'utf8'));
await inc.evals.report(results, { create: { ref: 'alice/haiku@v3', model: 'claude-opus-5-5' } }); // or { runId }Results are sent in chunks of 500. See Evals and the CI gate.
Device login
const flow = await inc.device.start({ deviceName: 'my tool' });
console.log(flow.userCode, flow.verificationUrl);
if ((await flow.wait()) === 'approved') inc.setToken(flow.token);The SDK mints the token itself; the server only ever sees its SHA-256 and a short prefix.
Verifying webhooks
verifyWebhookSignature(secret, header, body, toleranceSec = 300) checks the Incantory-Signature header of a webhook delivery. Pass the raw request body exactly as received. It returns a Promise<boolean> (it uses Web Crypto, so it also runs in browsers and edge runtimes) and never throws on malformed input; timestamps older than toleranceSec are rejected to stop replays.
import { verifyWebhookSignature } from '@incantory/sdk';
export async function POST(req: Request) {
const body = await req.text();
const ok = await verifyWebhookSignature(process.env.INCANTORY_WEBHOOK_SECRET!, req.headers.get('incantory-signature') ?? '', body);
if (!ok) return new Response('bad signature', { status: 401 });
const event = JSON.parse(body);
// ...
return new Response(null, { status: 204 });
}Errors and retries
Every failure is an IncantoryError with status (0 when there was no response), code (the API's stable code, or an SDK code such as network_error, timeout, integrity_error, lock_mismatch, label_not_found, offline_cache_miss, invalid_ref), message, requestId, details and retryAfter. Retry behaviour is described in Rate limits and errors.
Cache
The disk cache lives under <cacheDir>/v1/ and is shared with the Python SDK: immutable versions by content hash, @vN refs, and ETag-revalidated label and latest lookups (keyed per token fingerprint, never the token itself).