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";