Developers

SDK starter

Asyrx is a single-endpoint HTTP contract, so a production-grade client is small. This is the reference architecture we recommend for wrapping it inside your own platform.

Design

Five properties of a good Asyrx client

Thin and typed

The SDK mirrors the wire contract exactly. It never renames fields, never derives its own scores, and never infers an authorization outcome.

Idempotent by default

Every assess call carries an Idempotency-Key so a retry after a network failure returns the original assessment instead of creating a duplicate.

Bounded timeouts

Client timeouts sit above the service budget so the caller sees a structured response rather than an aborted socket.

Structured errors

Non-2xx responses become a typed AsyrxApiError carrying code, request_id and retryability — never a bare string.

Fail-safe posture

If an assessment cannot be completed, the SDK surfaces that explicitly. The calling system decides what to do; Asyrx never approves, denies, or executes the action.

Reference implementation

A typed TypeScript client

Copy this into your codebase and adapt the transport to your platform's conventions.

asyrx.ts

// asyrx.ts — reference client
export interface AsyrxAssessRequest {
  action_type: string;
  environment: "production" | "staging" | "development" | "sandbox";
  agent_id?: string;
  target_system?: string;
  financial_value?: number;
  currency?: string;
  entities_affected?: number;
  records_affected?: number;
  destination?: string;
  data_classification?: "public" | "internal" | "confidential" | "restricted" | "regulated";
  reversibility?: "fully_reversible" | "mostly_reversible" | "partially_reversible" | "difficult_to_reverse" | "irreversible" | "unknown";
  privilege_change?: boolean;
  production_impact?: boolean;
  action_description?: string;
  target_resource?: string;
  metadata?: Record<string, unknown>;
}

export class AsyrxApiError extends Error {
  constructor(
    readonly code: string,
    readonly status: number,
    readonly requestId: string | null,
    readonly retryable: boolean,
    message: string,
  ) {
    super(message);
    this.name = "AsyrxApiError";
  }
}

export interface AsyrxClientOptions {
  apiKey: string;
  baseUrl?: string;
  timeoutMs?: number;
  maxRetries?: number;
}

const RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]);

export class AsyrxClient {
  private readonly baseUrl: string;
  private readonly timeoutMs: number;
  private readonly maxRetries: number;

  constructor(private readonly options: AsyrxClientOptions) {
    this.baseUrl = options.baseUrl ?? "https://www.asyrx.com";
    this.timeoutMs = options.timeoutMs ?? 10_000;
    this.maxRetries = options.maxRetries ?? 2;
  }

  async assess(request: AsyrxAssessRequest, idempotencyKey = crypto.randomUUID()) {
    let attempt = 0;

    for (;;) {
      attempt += 1;
      try {
        const response = await fetch(`${this.baseUrl}/api/v1/actions/assess`, {
          method: "POST",
          headers: {
            "content-type": "application/json",
            authorization: `Bearer ${this.options.apiKey}`,
            "idempotency-key": idempotencyKey,
          },
          body: JSON.stringify(request),
          signal: AbortSignal.timeout(this.timeoutMs),
        });

        const requestId = response.headers.get("asyrx-request-id");
        const body = (await response.json()) as Record<string, any>;

        if (response.ok) return body;

        const code = String(body?.error?.code ?? "UNKNOWN_ERROR");
        const retryable = RETRYABLE_STATUS.has(response.status);
        if (retryable && attempt <= this.maxRetries) {
          await sleep(backoffMs(attempt, response.headers.get("retry-after")));
          continue;
        }

        throw new AsyrxApiError(
          code,
          response.status,
          requestId,
          retryable,
          String(body?.error?.message ?? "Asyrx request failed"),
        );
      } catch (cause) {
        if (cause instanceof AsyrxApiError) throw cause;
        if (attempt <= this.maxRetries) {
          await sleep(backoffMs(attempt, null));
          continue;
        }
        throw new AsyrxApiError(
          "NETWORK_ERROR",
          0,
          null,
          true,
          cause instanceof Error ? cause.message : "Network failure",
        );
      }
    }
  }
}

function backoffMs(attempt: number, retryAfter: string | null) {
  if (retryAfter) return Number(retryAfter) * 1000;
  return Math.min(2 ** attempt * 250, 4_000) + Math.random() * 250;
}

function sleep(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

Usage

Calling it from your action pipeline

usage.ts

import { AsyrxClient, AsyrxApiError } from "./asyrx";

const asyrx = new AsyrxClient({ apiKey: process.env.ASYRX_API_KEY! });

try {
  const assessment = await asyrx.assess({
    action_type: "bulk_contact_export",
    environment: "production",
    agent_id: "agent_sales_ops_01",
    target_system: "salesforce",
    records_affected: 48_000,
    destination: "unknown-vendor-files.example.net",
    data_classification: "confidential",
    reversibility: "irreversible",
  });

  // Your platform decides. Asyrx only informs.
  if (assessment.recommendation === "recommend_human_review") {
    await queueForApproval(assessment);
  }
} catch (error) {
  if (error instanceof AsyrxApiError) {
    console.error(error.code, error.requestId, error.retryable);
  }
  // Apply your own fail-open or fail-closed policy here.
}

Note the shape of the integration: Asyrx returns risk intelligence and a recommendation, and your platform decides what happens next. Asyrx never authorizes, approves, denies, blocks, or executes the action.

Generate your own client from the spec.

View the OpenAPI specification →