For agents

Integrate a protected action

Build the gate around one exact payout-destination change. Start with a sandbox and a mock destination store. Run your existing authentication, account permissions, CSRF controls, and business validation before requesting GrayPass authorization.

The TypeScript snippets below illustrate the integration in stages. pending is your durable application record, graypass is a configured runtime Node client, and controlPlane is a separate client with an admin credential for setup. Run the quickstart for the complete application, then use these excerpts to adapt each stage.

Freeze the business request first

Resolve the merchant and GrayPass subject from your authenticated session. Parse and validate the proposed destination on the backend. Store an immutable pending request with a logical ID, authenticated owner, subject, merchant, exact destination, expected audience, and an expiry. Accept the proposed destination from the browser and resolve the subject, merchant ownership, and action key on the backend.

Bind a digest of the exact normalized destination into the GrayPass action context. The normalization used to create the digest must be the same normalization used by the mutation. Prefer an opaque identifier or keyed digest when an ordinary hash would expose a low-entropy value to guessing.

TypeScript
import { createHash } from 'node:crypto';

// `pending.destination` is already validated and frozen by your backend.
const destinationFingerprint = createHash('sha256')
  .update(JSON.stringify({
    version: 1,
    destination: pending.destination,
  }))
  .digest('hex');

const resource = { type: 'merchant_account', id: pending.merchantId };
const context = { destination_fingerprint: destinationFingerprint };

The snippet's pending record represents your application's durable request state. Do not rebuild it from a later browser submission when resuming an approved action. Keep bank details out of GrayPass context and logs.

Register the action

Use an admin-scoped credential or the Console for setup. Use a server-scoped credential for runtime authorization.

TypeScript
const action = await controlPlane.registerAction({
  key: 'payout.destination.change',
  name: 'Change payout destination',
  consequence: 'Future payouts go to a different bank account.',
  riskClass: 'high',
  resourceType: 'merchant_account',
  contextSchema: {
    type: 'object',
    required: ['destination_fingerprint'],
    properties: {
      destination_fingerprint: {
        type: 'string', minLength: 64, maxLength: 64,
      },
    },
    additionalProperties: false,
  },
  allowedAudiences: ['payments-app'],
});

The action starts in shadow mode. Review its active policy before testing authorization. Actions and policies explains policy creation, activation, and mode configuration. Use sandbox enforce mode for the mock operation, then follow Production operation for evaluation, monitoring, and recovery.

Establish the subject and evidence session

Create or retrieve the subject on the backend with createSubject. Mint a short-lived client token for that subject and a registered origin using createClientToken. Run enrollment with purpose: 'enroll'; complete it from the backend only after the server reports enrollment_ready. Start an observation session with purpose: 'observe' for subsequent actions.

Your backend must associate the supplied observation session with the signed-in subject. The browser SDK exposes observer.session; it never exposes a secret server credential or requests a decision. Flush pending evidence before an action, then inspect accepted coverage and the policy decision.

Request the decision

TypeScript
const decision = await graypass.authorize({
  subject: pending.subjectId,
  session: pending.observationSessionId,
  action: 'payout.destination.change',
  resource,
  context,
  audience: 'payments-app',
  idempotencyKey: pending.id,
  requestedAt: pending.requestedAt,
});

Persist the authorization ID, decision.verification.nonce, and decision.verification.actionHash with the pending request. The SDK computes that hash from the submitted request and checks the server response against it. For explicit independent recomputation, use actionHash with the expected environment ID from your trusted configuration. Use the saved request as the authoritative source for verification.

Test and shadow decisions record the policy result for evaluation. In the sandbox example, the destination stays unchanged; in a live shadow study, your existing application path executes and supplies its outcome labels.

Challenge and resume

For challenge, send the browser the challenge ID, your opaque pending-request ID, and the immutable intent saved by your backend for that request, including the destination and merchant needed for meaningful confirmation. Use observer.completeChallenge() with a confirmation callback that displays that saved intent alongside details.display. The GrayPass display includes resource/context digests. Pair them with the exact destination saved by your backend for this request. For a passkey challenge, this callback runs before the ceremony. For a sandbox simulation, fetch challengeDetails() and show the saved intent yourself before calling completeChallenge(id, { method: 'sandbox_confirm' }); this method uses the confirmation UI you present before calling it.

On resume, authenticate the user again and look up the pending request by its server-side ownership. Resolve the returned authorization ID through graypass.getAuthorization(). Require it to be the original authorization or a valid child for that pending authorization, with the same subject, action, audience, request binding, environment, and expected policy context. A browser-provided child ID alone is insufficient.

The authorize() response carries proof as a compact string. The trace from getAuthorization() carries it as trace.proof?.compact. Preserve this distinction when handling direct allows and resumed allows.

Verify, consume, and execute

For an enforce-mode allow, use the independently stored request hash:

TypeScript
const expected = {
  audience: pending.audience,
  expectedAction: pending.actionKey,
  expectedActionHash: pending.actionHash,
};
const offline = await graypass.verify(proof, expected);
if (!offline.valid) throw new Error(`Proof refused: ${offline.reason}`);

const online = await graypass.verifyOnline(proof, {
  ...expected,
  consume: true,
});
if (!online.valid) throw new Error(`Proof inactive: ${online.reason}`);

Only after both checks succeed may your application execute the frozen destination change. Use a transaction or a downstream idempotency key to ensure your business operation runs at most once for the logical request. Serialize concurrent resumes for that pending request. Do not turn hold, deny, verifier exceptions, transport timeouts, or an ambiguous consumption response into permission.

The crash boundary

Proof consumption and your business mutation are separate transactions. A crash after consumption but before mutation can leave a consumed proof and an unexecuted operation. Your own ledger must distinguish pending, executing, completed, and uncertain attempts. If a downstream mutation may have succeeded, reconcile it using your business idempotency record. For an unexecuted operation whose proof has completed its single use, obtain fresh authorization. Associate successful consumption with the specific attempt in your ledger before execution.

The downloadable quickstart uses a durable SQLite ledger for its local mutation. For a remote business service, integrate its idempotency and reconciliation controls to coordinate proof consumption with the separately committed operation.

Report the outcome

After confirming the business result, call reportOutcome(authorizationId, 'executed') or the appropriate non-execution or review outcome. Save the terminal application result and its outcome report together in a durable transaction, then send reports through a retryable outbox. This also applies to not_executed reports from test and shadow decisions: a transport failure must not lose the evaluation outcome or restart authorization. An outcome-reporting retry must never repeat the payout change. Record customer overrides with the corresponding attributed outcome, separately from GrayPass policy allows.

Test changed destinations, wrong users, foreign environments, stale requests, repeated nonces, duplicate resumes, expired and revoked proofs, provider timeouts, and process crashes. The Quickstart supplies the runnable sandbox companion; Production operation covers rollout and recovery.