Waysafe

SDK reference

Source: packages/sdk

A thin, typed HTTP client. Every method here is a direct wrapper around one REST call — nothing here is a framework, a UI, or a required flow.

Install and instantiate

@waysafe/sdk is real, public source — packages/sdk in this repo, the same package the quickstart above already ran, straight from a clone. It isn’t published to npm yet because there’s no hosted API yet for a published package to point at — nothing to publish for, not something being withheld. Once a hosted deployment exists, this is what installing and pointing it at one will look like:

# not yet published -- this is what it will look like once a hosted API exists
npm install @waysafe/sdk

import { Waysafe } from "@waysafe/sdk";

const waysafe = new Waysafe({
  baseUrl: "https://api.your-waysafe-deployment.example",
  apiKey: process.env.WAYSAFE_API_KEY, // an org or agent credential
});

Method reference

The public surface of @waysafe/sdk. For the full request/response shapes on the calls that decide whether money moves, see the raw REST reference below.

MethodDescriptionThrows
compileMandate(request: CompileMandateRequest): Promise<CompileMandateResponse>Compiles a natural-language instruction into a policy. Does not create a mandate — a compiled result must be shown to the principal and authenticated before it can authorize anything.ValidationError, NetworkError, UnauthorizedError
createPrincipal(request: CreatePrincipalRequest): Promise<PrincipalDetail>Registers the person (or org) a mandate delegates authority over.ValidationError, UnauthorizedError, NetworkError
getPrincipal(principalId: string): Promise<PrincipalDetail>Fetches a principal by id.NotFoundError, UnauthorizedError, NetworkError
createMandate(request: CreateMandateRequest): Promise<CreatedMandate>Persists a compiled, confirmed policy as a mandate, status PENDING_AUTHENTICATION. Not usable by authorize() until the principal authenticates it.ValidationError, NotFoundError, NetworkError
getMandate(mandateId: string): Promise<MandateDetail>Fetches the full mandate: policy, intent text, assumptions, and bound agents.NotFoundError, NetworkError
listMandates(options?: { limit?: number }): Promise<MandateListItem[]>Lists mandates in your organization, most-recent-first.UnauthorizedError, NetworkError
getInstrument(instrumentId: string): Promise<InstrumentDetail>Fetches a rail-specific spend instrument — the actor a rail-initiated decision is attributed to when actor_kind is "instrument".NotFoundError, UnauthorizedError, NetworkError
getMandateAuthenticationOptions(mandateId: string): Promise<MandateAuthenticationOptions>Returns whether the principal needs to register a first passkey or sign with one on file — hand challenge/rp_id/origin to your own frontend's WebAuthn call.NotFoundError, NetworkError
verifyMandateAuthentication(mandateId: string, request: VerifyMandateAuthenticationRequest): Promise<MandateAuthenticationResult>Completes the registration or authentication ceremony your frontend started. Only mode "authenticate" activates the mandate.ValidationError, NotFoundError, NetworkError
authorize(request: AuthorizeRequest): Promise<AuthorizationDecision>Asks whether an agent may take an action under a mandate. DENY and STEP_UP are normal return values, not errors — this only throws when the request couldn't be decided at all.NoActiveMandateError, IdempotencyConflictError, ValidationError, NetworkError
execute(decision: ExecutableDecision, params: ExecuteParams): Promise<AuthorizationDecision>Executes an authorized action against a payment rail. Only accepts what asExecutable() returns — a DENIED or still-pending decision can't be passed here even by mistake.ExecutionRejectedError, UnknownRailError, AuthorizationStatusConflictError, NetworkError
verify(authorizationId: string): Promise<AuthorizationDecision>Fetches the current state of an authorization — the receipt.NotFoundError, NetworkError
resolveStepUp(authorizationId: string, approver: { agentId: string; principalId: string; mandateId?: string; idempotencyKey?: string }): Promise<AuthorizationDecision>Resolves a needs-higher-authority step-up as an approver mandate. Never a bare approve/decline flag — the real evaluate() engine runs against the approver's own policy. ALLOW resolves to STEP_UP_APPROVED; DENY or STEP_UP resolves to STEP_UP_DECLINED. Already resolved or expired: replays the recorded outcome, never re-evaluates.StepUpResolutionRejectedError, AuthorizationStatusConflictError, NetworkError
listAuthorizations(options?: { limit?: number }): Promise<AuthorizationDecision[]>Lists authorizations in your organization, most-recent-first.UnauthorizedError, NetworkError
listReasonCodes(): Promise<{ code: ReasonCode; description: string }[]>Returns the reason-code dictionary with operator-facing prose. Branch on the code, not this text.UnauthorizedError, NetworkError
createAgent(request: CreateAgentRequest): Promise<CreatedAgent>Registers an agent.ValidationError, UnauthorizedError, NetworkError
listAgents(): Promise<AgentSummary[]>Lists agents in your organization.UnauthorizedError, NetworkError
createAgentKey(agentId: string, request: { name: string }): Promise<CreatedAgentKey>Mints an agent API key. The full key is shown exactly once in this return value — it is never retrievable again.NotFoundError, ValidationError, NetworkError
listKeys(): Promise<AgentKeySummary[]>Lists every key in your organization — agent keys and org credentials alike, never the full key.UnauthorizedError, NetworkError
listEvidence(options?: { subject?: string }): Promise<EvidenceRecord[]>Fetches the evidence chain for your organization, optionally filtered to one subject.UnauthorizedError, NetworkError
getEvidencePublicKey(): Promise<EvidencePublicKey>The Ed25519 public key every evidence event's signature is checked against. No credential required — a third party auditing a receipt has none. Pin this value once verified out-of-band.NetworkError
verifyEvidenceChain(): Promise<ChainVerificationResult>Asks the server to verify its own chain — hash consistency plus every signature. A convenience call that still trusts the server's own judgment.UnauthorizedError, NetworkError
asExecutable(decision: AuthorizationDecision): ExecutableDecision | nullThe only constructor for ExecutableDecision. Returns null for any status other than AUTHORIZED or STEP_UP_APPROVED — passing a DENIED decision to execute() is a compile error, not a runtime check.Never throws.
verifyEvidenceIndependently(events: EvidenceRecord[], publicKeyBase64: string): ChainVerificationResultVerifies a chain's hash consistency and every signature locally — no network call, no trust in this server's own judgment. Runs on events from listEvidence() and the key from getEvidencePublicKey().Throws if publicKeyBase64 is not a valid Ed25519 SPKI key.

Raw REST reference

The SDK is a thin wrapper — this is the wire format underneath, for the two calls that decide whether money moves. Shapes below are transcribed from apps/api/src/server.ts and packages/core/src/domain.ts.

POST /v1/authorizations

Request body:

{
  "agent_id": "agt_...",
  "principal_id": "prin_...",
  "mandate_id": "mdt_...",            // optional -- pins evaluation to one mandate
  "action": {
    "amount": 4200,                    // integer minor units -- $42.00, never a float
    "currency": "USD",
    "merchant": {
      "name": "Staples",               // agent's own claims -- optional, any/all of these
      "domain": "staples.com",
      "psp_account": "acct_...",
      "network_mid": "...",
      "mcc": "5943",
      "onchain_address": "0x..."
    },
    "category": "office_supplies",     // optional
    "description": "Copier paper, 10 reams",  // optional, shown on the receipt
    "attestations": {}                 // optional agent claims, evaluated as claims not facts
  },
  "idempotency_key": "a-client-generated-key-8-255-chars",  // optional, required in production
  "context": {}                        // optional, arbitrary caller context recorded on the receipt
}

merchant carries only what the agent asserts on this call. Which field a value sits in never upgrades its trust — a rail-attested psp_account or network_mid (one the payment rail’s own callback supplied, not one the agent typed here) verifies; the same field asserted directly on this request caps out at STEP_UP, exactly like an unverified name.

Trust belongs to each identifier, not to the merchant as a whole. Sending a real domain alongside an account id of your own does not launder the account id: a match produces ALLOW only when the identifier that matched is itself verified and no identifier on the request is unverified. The STEP_UP_MERCHANT_UNVERIFIED reason names which one failed, so a receipt distinguishes a laundered sibling from a wholly unknown merchant.

On the WebAuthn ceremony routes, a challenge carries the purpose it was issued for and that purpose is authoritative: answering an authentication challenge with a registration response (or the reverse) is a 400 ({"error":"challenge_purpose_mismatch"}), never silently accepted. Enrolling a second passkey for a principal that already has one additionally requires a fresh prior authentication with an existing credential.

Response status: 201 on a fresh decision, 200 if idempotency_key matches a prior request and the stored result is replayed, 404 ({"error":"no_active_mandate"}) if no active mandate covers this request, 409 ({"error":"idempotency_conflict"}) if the same key was reused with a different body.

The three decision shapes, all the same receipt envelope:

// decision: "ALLOW"
{
  "id": "auth_...",
  "actor_kind": "agent",               // or "instrument" for a rail-initiated decision
  "agent_id": "agt_...",
  "instrument_id": null,
  "principal_id": "prin_...",
  "mandate_id": "mdt_...",
  "mandate_version_id": "mdv_...",
  "policy_hash": "...",
  "decision": "ALLOW",
  "status": "AUTHORIZED",
  "reasons": [{ "code": "ALLOW_WITHIN_MANDATE", "message": "..." }],
  "action": { /* echoes the request's action */ },
  "merchant": { /* the engine's resolved view -- trust level, refs */ },
  "idempotency_key": "...",
  "step_up_expires_at": null,
  "created_at": "2026-09-16T03:14:03.431Z",
  "decided_at": "2026-09-16T03:14:03.431Z"
}

// decision: "DENY" -- same envelope, status: "DENIED".
// reasons can carry more than one code: a request can violate several
// constraints at once (e.g. an unlisted merchant AND over the per-
// transaction limit), and every one that applies is returned, not just
// the first.
"reasons": [
  { "code": "DENY_MERCHANT_NOT_ALLOWLISTED", "message": "..." },
  { "code": "DENY_TRANSACTION_LIMIT_EXCEEDED", "message": "..." }
]

// decision: "STEP_UP" -- status: "PENDING_STEP_UP", step_up_expires_at
// set. Resolved at POST /v1/authorizations/:id/step-up (approve/decline);
// approving does not execute -- POST .../execute is the separate step.
"status": "PENDING_STEP_UP",
"step_up_expires_at": "2026-09-16T03:29:03.431Z"

POST /v1/mandates/compile

Request body:

{
  "intent_text": "Spend up to $500/month on office supplies from Amazon or Staples.",
  "timezone": "America/New_York",      // optional, defaults from the environment
  "currency": "USD",                   // optional
  "default_ttl_hours": 720             // optional
}

Three response shapes, all HTTP 200 except failed:

// status: "compiled"
{ "status": "compiled", "policy": { /* ... */ }, "policy_hash": "...",
  "confirmation": { /* ... */ }, "diagnostics": { "compiler": "...", "attempts": 1, "duration_ms": 812 } }

// status: "needs_clarification" -- HTTP 200, not an error. Asking is a
// valid, expected outcome: the compiler never invents a spending ceiling
// the principal didn't state.
{ "status": "needs_clarification",
  "clarifications": [
    { "path": "/limits/per_transaction", "question": "What's the most this can spend in one transaction?",
      "suggested_default": 15000, "rationale": "The instruction gave a monthly cap but no per-transaction limit." }
  ],
  "draft": { /* best-effort partial policy */ }, "diagnostics": { /* ... */ } }

// status: "failed" -- HTTP 422, the only error status of the three.
{ "status": "failed", "error": "policy_compilation_failed", "issues": [ /* ... */ ], "diagnostics": { /* ... */ } }

GET /v1/evidence/public-key

No credential required — a third party auditing a receipt has none. One signing key covers every organization on a given deployment, so there’s one key to publish, not one per tenant. Pin this value once verified out-of-band; a server that could change it at will could sign anything.

{ "algorithm": "Ed25519", "public_key": "MCowBQYDK2VwAyEA..." }

Pair with GET /v1/evidence (returns the same EvidenceRecord[] shape listEvidence() does) and verifyEvidenceIndependently — or its self-contained node:crypto equivalent — to verify locally, with zero trust in the server that produced the data. See /proof for a worked example against one real captured run.

Reason codes

Source: packages/core/src/reason-codes.ts

A public API (additive-only, never renamed) — every decision carries at least one of these. Branch on the code, not the description. Source: packages/core/src/reason-codes.ts.

DecisionCodeDescription
ALLOWALLOW_WITHIN_MANDATEThe action is within the delegated authority.
DENYDENY_NO_ACTIVE_MANDATENo active mandate delegates this authority to this agent.
DENYDENY_MANDATE_EXPIREDThe mandate has expired.
DENYDENY_MANDATE_REVOKEDThe mandate was revoked by the principal.
DENYDENY_MANDATE_SUPERSEDEDThe mandate version referenced has been replaced by a newer version.
DENYDENY_MANDATE_NOT_AUTHENTICATEDThe mandate was never authenticated by the principal.
DENYDENY_AGENT_NOT_BOUNDThis agent is not bound to the mandate.
DENYDENY_AGENT_SUSPENDEDThis agent is suspended.
DENYDENY_PRINCIPAL_MISMATCHThe mandate belongs to a different principal than the one named.
DENYDENY_CURRENCY_NOT_PERMITTEDThe mandate does not permit this currency.
DENYDENY_TRANSACTION_LIMIT_EXCEEDEDThe amount exceeds the per-transaction limit.
DENYDENY_CUMULATIVE_LIMIT_EXCEEDEDThe amount would exceed a cumulative spending limit.
DENYDENY_VELOCITY_LIMIT_EXCEEDEDThe action would exceed a transaction-count limit.
DENYDENY_MERCHANT_BLOCKEDThe merchant is explicitly blocked by the mandate.
DENYDENY_MERCHANT_NOT_ALLOWLISTEDThe merchant is not on the mandate's allowlist.
DENYDENY_CATEGORY_BLOCKEDThe category is explicitly blocked by the mandate.
DENYDENY_CATEGORY_NOT_ALLOWLISTEDThe category is not on the mandate's allowlist.
DENYDENY_OUTSIDE_TIME_WINDOWThe action falls outside the mandate's permitted time window.
DENYDENY_CONSTRAINT_NOT_SATISFIEDA required constraint of the mandate is not satisfied by this action.
DENYDENY_MERCHANT_UNRESOLVEDThe merchant could not be resolved to a verifiable identity.
DENYDENY_STEP_UP_SELF_APPROVALA mandate cannot resolve its own step-up; the resolving credential must belong to a different, authorized approver mandate.
DENYDENY_MANDATE_NOT_AN_APPROVERThis mandate is not named in the principal mandate's list of approvers.
DENYDENY_APPROVER_WOULD_ESCALATEThe approver's own policy also requires escalation for this action; approval authority is single-level and cannot chain to a further approver.
DENYDENY_APPROVER_CYCLEThis mandate's approvers would form a cycle with a mandate that already exists.
DENYDENY_STEP_UP_EXPIREDThe step-up's time limit had already passed, so it can no longer be approved. Distinct from DENY_MANDATE_EXPIRED: the mandate's authority is intact, and the window to resolve this particular step-up has closed.
DENYDENY_ASSET_NOT_IN_REGISTRYThe (chain, token contract address) pair is not one this deployment can evaluate or settle. Matching is by address, never by symbol.
DENYDENY_ASSET_DECIMALS_MISMATCHThe counterparty declared a decimal scale that disagrees with the asset registry's. A loud denial, never a silent correction.
DENYDENY_SETTLED_WITHOUT_AUTHORIZATIONThe payment rail settled this transaction without ever asking Waysafe to approve it -- a card-network force capture. Recorded as a DENY on a row whose status is EXECUTED: what Waysafe would have said, and the fact that the money moved anyway.
DENYDENY_SETTLED_ABOVE_AUTHORIZATIONThe settled amount exceeded what Waysafe authorized -- an overcapture, real on amount-controllable categories such as fuel and restaurants. The authorized portion settles normally; the excess carries this code.
DENYDENY_DECISION_NOT_RECORDEDThe decision could not be durably recorded, so it was not authorized. The decision, its ledger hold and its evidence event commit in one transaction; if that fails, nothing survives and the rail is answered with a decline.
DENYDENY_RESOURCE_URL_NOT_PERMITTEDWaysafe refused to fetch the resource URL supplied, so no payment requirement was ever read. Production permits HTTPS to public addresses only, with the connection pinned to the address it validated and every redirect re-checked.
DENYDENY_ASSET_UNSPECIFIEDThe payment requirement named no token contract address, so no asset could be resolved.
STEP_UPSTEP_UP_AMOUNT_THRESHOLDThe amount is above the mandate's step-up threshold.
STEP_UPSTEP_UP_CUMULATIVE_THRESHOLDCumulative spend is above the mandate's step-up threshold.
STEP_UPSTEP_UP_MERCHANT_NOT_ALLOWLISTEDThe merchant is not on the allowlist and the mandate requires approval for unlisted merchants.
STEP_UPSTEP_UP_CATEGORY_NOT_ALLOWLISTEDThe category is not on the allowlist and the mandate requires approval for unlisted categories.
STEP_UPSTEP_UP_MERCHANT_UNVERIFIEDThe merchant identity was asserted but not verified, so human approval is required.
STEP_UPSTEP_UP_FIRST_TIME_MERCHANTThis is the first transaction with this merchant under this mandate.
STEP_UPSTEP_UP_MANDATE_REQUIRES_REAUTHThe mandate requires re-authentication before further actions.

Evidence event types

Every type this codebase appends to the signed chain, and what each attaches to. A chain is organization-scoped: one sequence, one hash link per entry, with subject_type and subject_id naming what the entry is about.

Decisions made through the API are in this chain too. That was not true until a second adversarial review found it: the rail-initiated paths each appended their own decision evidence, and POST /v1/authorizations appended nothing. A step-up that expires still writes no event. That is a known gap, named in the threat model.

TypeSubjectWhat it records
authorization.decidedauthorizationAn agent-path decision: POST /v1/authorizations returning ALLOW, DENY or STEP_UP. Carries the decision, its reason codes, the amount, the mandate version and policy hash it was decided against, the resolved merchant, whether it reserved budget, and any step-up expiry. Appended in the same transaction as the decision and its hold.
agent_key.verifiedagentAn agent credential was presented and matched the agent it claimed to be.
agent_key.rejectedagentA credential was presented and did not match. Written unconditionally, before any decision, so an attempt against a mandate that does not exist still leaves a record.
step_up.approvedmandateA pending step-up was approved by a separate, principal-named approver mandate's own credential. One event per mandate involved, so both chains show it.
step_up.declinedmandateA pending step-up was declined. Any reservation is released.
step_up.resolution_rejectedmandateA resolution attempt was refused — self-approval, an expired step-up, or a spending mandate that lapsed while the step-up was open.
execution.completedauthorizationAn authorized payment was executed on a rail. Carries the provider, its reference, and the fee it took.
execution.rejectedauthorizationA payment adapter refused the execution. No capture is written.
refund.appliedauthorizationA refund was credited back to the budget. The provider's cumulative refunded total arrives on every update; only the delta is credited.
mandate.authenticatedmandate_versionThe principal authenticated this policy version over WebAuthn, freezing it.
mandate.authentication_rejectedmandate_versionA WebAuthn ceremony for this version was refused — a purpose mismatch, a bad origin, or an unknown credential.
mandate.card_issuing_terms_acceptedmandate_versionThe cardholder's own acceptance of Stripe's Issuing terms, sourced only from a real authentication ceremony and never synthesized.
enforcement.stripe_issuing.decisionauthorizationA card authorization decided in the network's real-time window.
enforcement.stripe_issuing.releasedauthorizationA reversed or expired card authorization's hold released, exactly once.
enforcement.stripe_issuing.capturedauthorizationA card authorization settled. Records the amount that SETTLED, which a partial capture makes smaller than the amount authorized.
enforcement.stripe_issuing.unauthorized_settlementauthorizationA force capture: the network cleared a settlement it never presented for approval. Carries the raw merchant_data a dispute needs and would_have_decided — what the engine returns when the settlement is put to it after the fact.
enforcement.stripe_issuing.over_authorized_settlementauthorizationAn overcapture: the settled amount exceeded what was approved. Same shape as above, with the excess named.
enforcement.x402.decisionauthorizationAn on-chain payment decided before any co-signature. Carries the atomic amount alongside the cents the budget was charged, since cents round up.