TypeScript SDK

@veriticity/sdk is the official TypeScript client for the Veriticity API: typed purchase decisions, typed errors, webhook signature verification and discriminated event types, with zero runtime dependencies.

@veriticity/sdk is the official TypeScript client for the Veriticity API. It is a consumer of the same public API this site documents — every method maps to one endpoint, and nothing it returns has been reshaped on the way through.

It is optional. The API is plain HTTP with JSON, and every example on this site is also shown as raw curl. Use the SDK if you are writing TypeScript and would like typed decisions, typed errors and a webhook verifier you do not have to write yourself.

Install#

npm
npm install @veriticity/sdk

ESM only, with zero runtime dependencies. Installing it adds one package to your tree and nothing else.

Authenticate#

The SDK accepts the same two credentials the API does: an API key, or an OAuth access token from a connection somebody authorised.

API key
import { Veriticity } from "@veriticity/sdk";

const veriticity = new Veriticity({
  apiKey: process.env.VERITICITY_API_KEY!,
});
OAuth access token
// An OAuth access token from a connection somebody authorised.
const veriticity = new Veriticity({
  accessToken: "cmp_oat_...",
});

// Or a function, called before every request. Access tokens last 60 minutes,
// so a client built once at module scope with a string stops working after an
// hour.
const veriticity = new Veriticity({
  accessToken: () => tokenStore.currentAccessToken(),
});

An API key bound to an agent and an organisation-scoped key look identical — both begin cmp_live_, and the difference between them is an authority the server holds. The SDK cannot check it locally, so each method’s documentation says which it needs and the server returns api_key_not_bound_to_agent or api_key_not_organization_scoped when it is not met.

MethodCredentialOAuth scope
purchases.submitAPI key bound to an agentpurchase:request
purchases.getAPI key bound to an agentpurchase:read
simulations.currentPoliciesEither kindpurchase:check
Everything elseOrganisation-scoped API key

The SDK does not implement OAuth authorisation or refresh. It consumes an access token; obtaining one is your application’s job, for the reasons set out in Authentication.

Your first purchase#

purchases.submit
const result = await veriticity.purchases.submit({
  merchant: "Acme Cloud",
  reason: "Renew build pipeline compute credits",
  amountMinor: 7500n,
  currency: "GBP",
});

switch (result.decision.outcome) {
  case "APPROVED":
    // Go ahead. result.decision.explanation says why.
    break;

  case "REJECTED":
    // A refusal is a successful call, not an exception.
    break;

  case "REQUIRES_APPROVAL":
    // A person has been asked. Wait for the webhook, or poll:
    //   await veriticity.purchases.get(result.purchaseRequest.id)
    break;
}

A refusal is not an exception. Veriticity was asked a question and answered it, so a REJECTED decision resolves normally. Branch on decision.outcome, never on the HTTP status.

Money#

amountMinor accepts a string, a number or a bigint and is always sent as a decimal string. It is always a string coming back, and the SDK never converts it to a JavaScript number — JSON numbers lose precision above 253, and this is money.

Check before you buy#

simulations.currentPolicies
const check = await veriticity.simulations.currentPolicies({
  merchant: "Acme Cloud",
  reason: "Renew build pipeline compute credits",
  amountMinor: "7500",
  currency: "GBP",
});

if (check.simulatedDecision.outcome === "REJECTED") {
  // Nothing was recorded, no budget was reserved, no evidence was written.
  return;
}

A simulation records nothing, reserves nothing and costs nothing, so an agent that is unsure should ask before it acts. It is not a guarantee — policy can change between the two calls, and only the submission decides.

Verify a webhook#

Verification needs no API credential and no HTTP client, so it lives in its own entry point. A receiver deployed to an edge runtime can import it without the rest of the package.

Next.js App Router
import { Webhooks, isKnownWebhookEvent } from "@veriticity/sdk/webhooks";

const webhooks = new Webhooks({
  secret: process.env.VERITICITY_WEBHOOK_SECRET!,
});

export async function POST(request: Request) {
  // Pass the Request itself. The verifier reads the raw bytes, because a
  // re-serialised body is not the body that was signed.
  const event = await webhooks.unwrap(request);

  if (!isKnownWebhookEvent(event)) {
    // A type added after this version of the SDK. Acknowledge and ignore.
    return new Response(null, { status: 204 });
  }

  switch (event.type) {
    case "purchase.approved":
      event.data.purchaseRequestId;
      break;

    case "budget.exhausted":
      event.data.policyName;
      break;

    case "webhook.test":
      event.data.message;
      break;
  }

  return new Response(null, { status: 204 });
}

During a rotation Veriticity signs each delivery with every live secret and sends both signatures, so a receiver holding either succeeds. Pass an array while you change over:

Rotation
new Webhooks({ secret: [nextSecret, currentSecret] });

Errors#

Error handling
import {
  InvalidRequestError,
  VeriticityApiError,
  VeriticityConnectionError,
} from "@veriticity/sdk";

try {
  await veriticity.purchases.submit(body);
} catch (error) {
  if (error instanceof InvalidRequestError) {
    // Every problem, not just the first.
    for (const issue of error.issues) {
      console.error(issue.path, issue.code, issue.message);
    }
  } else if (error instanceof VeriticityApiError) {
    // error.code is the contract. The classes are convenience over the status.
    console.error(error.code, error.status);
  } else if (error instanceof VeriticityConnectionError) {
    // Nothing reached Veriticity. Safe to retry with the same idempotency key.
  } else {
    throw error;
  }
}

Every failure the SDK throws extends VeriticityError. HTTP failures become a VeriticityApiError subclass chosen by status; transport failures become VeriticityConnectionError or VeriticityTimeoutError; a mistake the SDK can see without asking the server becomes VeriticityConfigError.

The classes are convenience. error.code is the same stable code documented under Errors, and the SDK invents none of its own. issues is always an array, so iterating it is always safe.

Errors carry status, code, message, issues, method, path and operationId — and deliberately carry no credential, no request headers, no request body and no full URL. An error object ends up in a log, and none of those belong there.

Retries and idempotency#

The SDK never retries. There are no rate limits, no 429 and no Retry-After to back off from, and a silently retried purchase submission would ask for a second decision, take a second budget hold and write a second row of permanent evidence.

purchases.submit
// The same key across every attempt is what makes a retry safe. The SDK never
// generates one, because a key has to survive your retry -- which means only
// you can mint it.
const result = await veriticity.purchases.submit(body, {
  idempotencyKey: attemptId,
});

result.replayed; // true when this was the stored answer to an earlier identical request

Idempotency is available on purchase submission and nowhere else, which the types enforce: an idempotencyKey passed to any other method is a compile error. A key over 255 characters is refused before the request is made.

Policies and pagination#

policies
// The API's own shape, returned unchanged.
const { policies, pagination } = await veriticity.policies.list({
  status: "ACTIVE",
  pageSize: 50,
});

// The one ergonomic addition: walk every page.
for await (const policy of veriticity.policies.listAll({ status: "ACTIVE" })) {
  console.log(policy.name);
}

The SDK preserves the API’s pagination rather than inventing one of its own, so what you get back is what the API reference describes. policies.listAll() is the single addition — an async generator that walks pages until the server reports the last one.

Webhook deliveries take a limit and nothing else, so there is no iterator for them; raise the limit to see more. There is no purchase-list method, because there is no purchase-list endpoint.

Runtimes#

RuntimeSupported
Node.js 20.19 or newerYes — the primary target
Next.js, both the Node and Edge runtimesYes
Serverless functionsYes
Cloudflare WorkersYes
Deno, BunYes
BrowserNo, deliberately

The SDK uses Web APIs only — fetch, URL, AbortSignal, TextEncoder and Web Crypto — so it runs unchanged everywhere above. A CommonJS project on a supported version of Node can require it.

What the SDK deliberately does not do#

  • No automatic retries, backoff, jitter or circuit breaking. Retry deliberately, with your own stable idempotency key.
  • No generated idempotency keys. A key has to be stable across your retries, which means only you can mint it.
  • No OAuth flow. It consumes an access token; it does not obtain, refresh or store one.
  • No payment execution. Veriticity decides. It does not hold funds and does not move money.
  • No logging or telemetry. The SDK writes nothing to stdout or stderr. Supply your own fetch to observe requests.
  • No browser support and no sandbox.

Where to go next#

  • Quickstart — the same first purchase, in cURL and TypeScript.
  • Purchases — decisions, reason codes and approval escalations in full.
  • Webhooks — the event catalogue, signature scheme and delivery behaviour.
  • Examples — complete integrations, with and without the SDK.
  • API reference — every endpoint the SDK calls, and the authority on all of it.