Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.tallyforagents.com/llms.txt

Use this file to discover all available pages before exploring further.

The Tally SDK turns every non-2xx response into a typed exception you can catch and branch on. The shape is consistent across the surface: every error is an instance of TallyError (or a subclass), and carries type, message, code?, status, and details? fields.

Class hierarchy

TallyError
├── AuthenticationError    // 401, 403
├── NotFoundError          // 404
├── ValidationError        // 422 (validation_failed)
├── RateLimitError         // 429
└── ConflictError          // 409
TallyError is the catch-all base. Anything the server returns that the SDK doesn’t have a specific subclass for surfaces as a plain TallyError.

Error shape

Every error carries these fields:
FieldTypeDescription
typestringThe server’s structured error type — unauthenticated, validation_failed, etc.
messagestringHuman-readable description. Safe to log; doesn’t include user data.
codestring | undefinedSpecific failure mode within type. See Error codes for payments.
statusnumberHTTP status code.
detailsunknownServer-provided structured context (often the failed Zod issues).

The classes

AuthenticationError — 401 / 403

class AuthenticationError extends TallyError {}
// type: "unauthenticated" or "forbidden"
Fires when the API key is missing, malformed, revoked, or out of its rotation grace window. Also fires when the key authorizes a different account or mode than the resource you’re acting on. Recovery: rotate the key. If the key was recently rotated and is still in its 24h grace window, the new key should already be working — the error indicates the old key is past the grace cutoff. Don’t retry with the same key.

NotFoundError — 404

class NotFoundError extends TallyError {}
// type: "not_found"
The resource you asked for doesn’t exist in this (account, mode). Often a sign of a mode mismatch — a tly_test_ key looking for a live-mode agent gets a NotFoundError, not a forbidden, because the resource is genuinely invisible to the key’s scope. Recovery: check the id, check the mode, log it, and surface “not found” in your UI as appropriate.

ValidationError — 400

class ValidationError extends TallyError {}
// type: "validation_failed"
The request payload failed server-side validation. err.details typically contains the structured Zod issue list. err.code carries a specific failure mode where applicable (amount_invalid, address_invalid).
try {
  await tally.payments.create({ ... });
} catch (err) {
  if (err instanceof ValidationError) {
    console.error("validation failed:", err.message, err.details);
  }
  throw err;
}
Recovery: fix the input. Don’t retry the same payload.
Most policy-related errors on payments.create() (per-tx max exceeded, daily cap exceeded, recipient not allowed, etc.) are returned as 403 forbidden from the server, which the SDK maps to AuthenticationError — not ValidationError. The naming is awkward; see Payments — Error codes for the full mapping.

RateLimitError — 429

class RateLimitError extends TallyError {}
// type: "rate_limited"
You’re sending requests faster than the account’s per-second ceiling. The response includes a Retry-After hint (the SDK doesn’t surface this on the error object today; check err.details if present). Recovery: exponential backoff. The pattern:
for (let attempt = 0; attempt < 5; attempt++) {
  try {
    return await tally.payments.create({ ... });
  } catch (err) {
    if (err instanceof RateLimitError) {
      await sleep(1000 * 2 ** attempt); // 1s, 2s, 4s, 8s, 16s
      continue;
    }
    throw err;
  }
}
Combine with an idempotency_key so retries are safe — see Payments.

ConflictError — 409

class ConflictError extends TallyError {}
// type: "conflict"
The request collides with existing state. The class is exported for completeness — the public v1 surface doesn’t throw it today, but it may surface from future endpoints (e.g. agents.delete() on an agent with active grants). Recovery: don’t retry without changing the request.

TallyError (base)

class TallyError extends Error {
  readonly type: string;
  readonly code?: string;
  readonly status: number;
  readonly details?: unknown;
}
Anything the SDK can’t map to a more specific class falls through as a TallyError. The most common cases are 5xx server errors (rare) and network failures bubbled up through fetch. Recovery: depends on status. 5xx should be retried with exponential backoff. Network errors (status: 0) should be retried with idempotency keys.

Idiomatic catch

import {
  AuthenticationError,
  ValidationError,
  RateLimitError,
  ConflictError,
  NotFoundError,
  TallyError,
} from "@tallyforagents/sdk";

try {
  await tally.payments.create({ ... });
} catch (err) {
  if (err instanceof AuthenticationError) {
    // Rotate the key, alert ops.
    return errorPage("auth_failed");
  }
  if (err instanceof AuthenticationError && err.code === "amount_too_large") {
    // Policy-layer violation; prompt to top up the permission.
    return promptForPermissionTopUp();
  }
  if (err instanceof RateLimitError) {
    return backoffAndRetry();
  }
  // Anything else → log and surface as a generic failure.
  console.error("unexpected tally error:", err);
  throw err;
}
Branch on the class first; branch on err.code second; let everything else propagate. Catching TallyError to silently swallow is almost always a bug.

Server error shape

If you’re curious what the SDK is parsing, every error response from Tally looks like this:
{
  "error": {
    "type": "validation_failed",
    "message": "Amount exceeds per-tx max.",
    "code": "insufficient_allowance",
    "details": { "max_per_tx_usdc": "10", "requested": "15" }
  }
}
The SDK’s makeError(payload, status) maps type → class — unauthenticated and forbidden both become AuthenticationError, not_found becomes NotFoundError, etc. That mapping is in packages/sdk/src/errors.ts if you want to see the exact switch.