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.

Every error response from the Tally API uses a consistent envelope. The HTTP status code tells you the rough category; the type and code in the body tell you the specifics.

Envelope

{
  "error": {
    "type": "validation_failed",
    "message": "Amount exceeds per-tx max.",
    "code": "amount_too_large",
    "details": { "max_per_tx_usdc": "10", "requested": "15" }
  }
}
Every error has type and message. code is set when there’s a more specific failure mode within a type (most payment policy violations carry one). details carries structured context when relevant — typically the failed Zod issue list for validation errors.

Error types

typeHTTPMeaning
unauthenticated401Missing, malformed, or invalid API key.
forbidden403The request authenticated, but a policy check rejected it. Inspect code.
not_found404The resource doesn’t exist in this (account, mode).
validation_failed400The request body or query failed validation. details carries the Zod issues.
conflict409The request collides with existing state. Not used by v1 endpoints today but reserved for future use.
rate_limited429The account is over its per-second ceiling. See Rate limits.
internal500A dependency failed (Privy, the chain). Retryable; see specific code for the cause.
The SDK maps each type to a typed exception class — see SDK errors. REST callers branch on type and code directly.

Codes on POST /v1/payments

The payments endpoint is the only v1 route with structured code values today. Branch on error.type first, error.code second.
codetypeMeaning
agent_not_foundnot_foundNo agent with that agent_id in this (account, mode).
wallet_not_foundnot_foundThe wallet address isn’t owned by this account, or is in the wrong mode.
permission_not_foundnot_foundThe agent has no active grant on that wallet.
amount_invalidvalidation_failedamount_usdc is malformed (non-decimal, too many decimal places, etc.).
address_invalidvalidation_failedwallet or to is not a valid EVM address.
amount_too_largeforbiddenExceeds the grant’s per-transaction max.
daily_cap_exceededforbiddenWould push the rolling 24-hour spend over the grant’s daily cap.
recipient_not_allowedforbiddento is not in the grant’s recipient allowlist.
contract_not_allowedforbiddenThe destination contract is not on the grant’s contract allowlist (USDC is the default).
permission_expiredforbiddenThe grant’s expires_at has passed.
privy_call_failedinternalPrivy’s API failed during signing. Retryable.
The forbidden-typed codes are policy violations, not auth failures. Surface them in your UI as “this permission doesn’t permit that,” not “your credentials are bad.”

Recovery patterns

TypeShould you retry?
unauthenticatedNo. Fix the key or rotate.
forbiddenNo, not the same way. Either fix the request shape (different to, smaller amount_usdc) or wait for a new grant.
not_foundNo. Either the id is wrong or the resource doesn’t exist yet.
validation_failedNo. Fix the input.
conflictMaybe. Depends on the specific endpoint; today no v1 route emits this.
rate_limitedYes, after Retry-After. Use exponential backoff.
internalYes, with an idempotency key. The cause is downstream.
Always read error.message for human context before deciding — the message often points at the specific field or constraint that failed.

Sample handlers

Branching on type and code

async function pay(input: PaymentInput) {
  const res = await fetch(`${baseUrl}/v1/payments`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(input),
  });

  if (res.ok) return res.json();

  const { error } = await res.json();

  switch (error.type) {
    case "forbidden":
      if (
        error.code === "amount_too_large" ||
        error.code === "daily_cap_exceeded"
      ) {
        // Policy violation — prompt to top up the permission.
        return notifyToTopUpPermission(error);
      }
      throw new PolicyError(error);
    case "rate_limited":
      await sleep(Number(res.headers.get("Retry-After") ?? 1) * 1000);
      return pay(input); // retry with the same idempotency key
    case "internal":
      // Tally or Privy hiccup; retry idempotently
      return pay(input);
    default:
      throw new ApiError(error);
  }
}

Logging unknown errors

When you encounter a type your handler doesn’t branch on, log the entire envelope and surface a generic failure to the user. Don’t drop the body — the details field often has exactly the info you need to diagnose later.