TypeScript SDK

npm install @allowly/sdk
# or
pnpm add @allowly/sdk

Requires Node.js 18+.

Initialize

import { Allowly } from "@allowly/sdk";

const allowly = new Allowly({ apiKey: process.env.ALLOWLY_KEY! });

Optional: derive a PII-safe user ID from email

Use your app's internal opaque user ID when you have one. If you need email-based lookup, hash the email locally before creating the authorization.

import { identifiers } from "@allowly/sdk";

const allowlyUserId = identifiers.fromEmail(user.email, {
  pepper: process.env.ALLOWLY_PII_PEPPER!,
});

The helper trims and lowercases only, returns an email_hmac:v1:... identifier, and never sends the raw email or pepper to Allowly. Keep the pepper stable and backed up; changing it changes derived user IDs. See PII-safe identifiers.

Create an authorization

When your user authorizes your agent, create an authorization and store the returned authorizationId.

const authorization = await allowly.authorizations.create({
  userId: allowlyUserId, // or your app's opaque internal ID
  agentId: "my-email-agent",
  scopes: [
    { name: "email.read" },
    { name: "email.send", constraints: { max_per_day: 5 } },
    { name: "candidate.delete" },
  ],
  requiresConfirmFor: ["email.send"],
  requiresEscalationFor: ["candidate.delete"],
  escalationTargets: { "candidate.delete": "manager" },
  expiresAt: "2026-12-31T00:00:00.000Z",
  metadata: { source: "onboarding_modal" },
});

await db.users.update(user.id, { allowlyAuthorizationId: authorization.authorizationId });
// authorization.receipt → { status: "pending", receiptId, url, readyAtEstimate }

Add a spend cap

On Pro and Enterprise workspaces, attach a budget cap to estimated-cost actions. The value is micro-USD.

const authorization = await allowly.authorizations.create({
  userId: allowlyUserId,
  agentId: "research-agent",
  scopes: ["llm.enrich"],
  expiresAt: "2026-12-31T00:00:00.000Z",
  budgetLimitMicros: 50_000_000, // $50.00
});

Check before every action

Every agent action must go through check(). This is what produces a signed receipt.

const result = await allowly.check({
  authorizationId: await db.users.getAuthorizationId(user.id),
  scopes: ["email.send"],
  resource: "gmail:thread:abc",
  context: { initiated_by: "user", origin: "chat" },
});
const scopeResult = result.results["email.send"];

if (scopeResult.decision === "allow") {
  await sendTheEmail();

} else if (scopeResult.decision === "confirm") {
  // User must approve before the action proceeds
  await promptUser(scopeResult.confirmPromptHint);
  await allowly.confirmations.approve(scopeResult.confirmNonce, { approved: true });
  // Re-call check() — now returns allow

} else if (scopeResult.decision === "escalate") {
  // Third-party approver must resolve before the action proceeds
  await routeToApprover(scopeResult.escalationTo, scopeResult.escalationId);
  await allowly.escalations.approve(scopeResult.escalationId, {
    resolvedBy: "manager:8821",
  });
  // Re-call check() — now returns allow once

} else {
  // decision === "deny" — branch on reason for the best user experience
  switch (scopeResult.reason) {
    case "rate_limit_exceeded":
      throw new Error("Daily limit reached — try again tomorrow.");
    case "authorization_revoked":
    case "authorization_expired":
      throw new Error(`Authorization no longer valid: ${scopeResult.reason}`);
    case "scope_not_authorized":
      throw new Error("Agent is not authorized for this action.");
    default:
      throw new Error(`Allowly denied: ${scopeResult.reason}`);
  }
}

scopeResult.receipt is a pending envelope — the signature is produced asynchronously by the server.

For a budgeted authorization, pass the estimated action cost:

const result = await allowly.check({
  authorizationId,
  scopes: ["llm.enrich"],
  estimatedCostMicros: 24_000,
});

const scopeResult = result.results["llm.enrich"];
if (scopeResult.reason === "budget_exceeded") {
  throw new Error("This action would exceed the authorization budget.");
}
if (scopeResult.budget) {
  console.log(scopeResult.budget.spentAfterMicros);
}

Fetch a signed receipt

// Poll until signed (default: 1s interval, 30s timeout)
if (scopeResult.receipt.status !== "pending") {
  throw new Error("Receipt is already signed");
}
const signedReceipt = await allowly.receipts.fetchSigned(scopeResult.receipt.receiptId);

// Or with custom options
const signedReceipt = await allowly.receipts.fetchSigned(scopeResult.receipt.receiptId, {
  pollInterval: 2,  // seconds
  timeout: 60,
});

Verify a receipt offline

import { fetchKeysDoc, loadKeysFromJson, verifyReceipt, VerificationError } from "@allowly/sdk";

const keysDoc = await fetchKeysDoc(workspaceId, {
  // Optional out-of-band hash pin for the keys document:
  // expectedSha256: "<sha256 hex>",
});
const keys = loadKeysFromJson(keysDoc);

try {
  await verifyReceipt(signedReceipt, keys);
  // valid — no network call needed
} catch (e) {
  if (e instanceof VerificationError) {
    console.error("Invalid receipt:", e.message);
  }
}

fetchKeysDoc() enforces HTTPS, caches the keys document in-memory for 5 minutes, validates the document shape before use, and optionally enforces an out-of-band SHA-256 pin.

Revoke an authorization

const result = await allowly.authorizations.revoke(authorizationId, { revokedBy: "user" });
// result → { authorizationId, revokedAt, receipt }

Handle errors

import { AllowlyAPIError } from "@allowly/sdk";

try {
  await allowly.check({ authorizationId, scopes: ["email.send"] });
} catch (e) {
  if (e instanceof AllowlyAPIError) {
    console.error(e.status, e.code, e.message);
  }
}

Types reference

import type {
  CheckResponse,
  BudgetInfo,
  ScopeCheckResultAllow,
  ScopeCheckResultDeny,
  ScopeCheckResultConfirm,
  ScopeCheckResultEscalate,
  AuthorizationCreateResponse,
  AuthorizationRevokeResponse,
  ConfirmationApproveResponse,
  EscalationResolveResponse,
  ReceiptEnvelope,
  ReceiptEnvelopePending,
  ReceiptEnvelopeSigned,
  PublicKey,
  KeyDocument,
} from "@allowly/sdk";