Skip to main content
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 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:

The classes

AuthenticationError — 401 / 403

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

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

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).
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

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:
Combine with an idempotency_key so retries are safe — see Payments.

ConflictError — 409

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)

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

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:
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.