Common errors - Terminal 3 Documentation

Errors

Errors come back as a JSON-RPC bad_request (HTTP 400) with { code: "bad_request", detail, request_id }. The SDK throws with detail — a human-readable message string, not a typed error object. Match on the substring shown below. User-authentication failures additionally carry a machine code at the front of detail (e.g. eth_authenticator_limit: …), so the SDK can branch with a single startsWith.

Tenant operations — register, maps, dispatch

You’ll see in detail Cause Fix
version <x> is not higher than current version <y> Re-registering a contract at a version that isn’t greater than the deployed one Bump the version passed to contracts.register
map already exists Re-running maps.create against an already-provisioned tenant Idempotent — safe to ignore on re-runs
map not found A map tail in kv_store::get / put doesn’t match what maps.create created Match the tails exactly between Create tenant KV maps and your Rust
canonical map name invalid: <reason> tail is empty, contains .., or starts with z: Pass only the local tail (e.g. "secrets") — the SDK prefixes z:<tid>:
quota exceeded: <dim> (e.g. quota exceeded: max_contracts) Hit a per-tenant quota Ask the cluster operator to raise the quota
access denied: <caller> cannot <op> map "<map>" The contract isn’t on the map’s readers / writers ACL tenant.maps.update to add the contract id to readers / writers
tenant is suspended The operator suspended your tenant Ask the operator to resume
host/http.egress_denied: host '<host>' is not in the authorised_hosts allowlist The contract called a host the caller’s agent_auth grant doesn’t authorize Add the host to the user’s grant (see Invoke your contract)
InsufficientCreditError on a metered function call from an agent identity Metered calls are charged against the calling identity’s own T3N credit balance — an agent DID’s balance is separate from its tenant’s, and starts at zero even when the tenant has plenty of test tokens. There’s currently no documented self-serve path for an agent (as opposed to a tenant) to acquire its own credit balance — token transfers are an admin-only action. Contact devrel@terminal3.io or the developer Telegram to get an agent identity funded.

Contract-authored errors are whatever your contract returns. The flight example, for instance, surfaces duffel_api_key not found in z:<tid>:secrets — populate it via the tenant SDK when the secrets map wasn’t seeded (see Seed API key into secrets map) — that’s the contract’s own message, not a platform error.

Generic / opaque failures (HTTP 500, no clear cause)

A bad_request gives you a substring to match on. A bare HTTP 500 with no further detail is a different situation — it usually isn’t your contract logic. Before assuming it’s a bug in your code, check these in order:

  1. Grab the request_id. Most 500 responses still include one — save it before you start changing code. If you end up asking for help, it’s the single most useful thing you can hand over.
  2. Re-check egress and ACLs first. A missing outbound-host grant or a missing map ACL entry can surface as a 500 instead of the more specific error you’d expect — re-read the egress and map ACL requirements before looking further.
  3. Retry once, deliberately. A single unhealthy node can return a 500 for requests that are otherwise correct. If an identical request succeeds on retry, it was very likely transient — no code change needed.
  4. If it’s consistent and reproducible, it’s more likely a platform-side issue than your integration. Report it in the developer Telegram with the request_id, rather than spending hours guessing at workarounds.

Common integration gotchas

These aren’t errors with a fixed message — they’re patterns that produce confusing behavior downstream, reported independently by multiple teams.

Gotcha What happens Fix
Hex-encoding a DID before building a map path format!("z:{}:secrets", hex::encode(&tenant_did())) double-encodes — tenant_did() already returns the string form, so wrapping it in hex::encode again produces a value that doesn’t match any map you actually created, and lookups silently fail to find it. Use tenant_did()’s return value directly in the map path — don’t re-encode it.
Omitting baseUrl when constructing a TenantClient TenantClient’s config accepts baseUrl as an optional field, but calls can still fail at request time without it, even after calling setEnvironment(). This is specific to TenantClientT3nClient (used for tenant, agent, and user sessions in the walkthrough) doesn’t take a baseUrl at all; it’s always resolved from the active environment. When constructing a TenantClient, always pass baseUrl: getNodeUrl() explicitly rather than relying on it being inferred.

Authentication & wallet linking

These come from the user/session contract during sign-in and addAuthMethod, with the code at the front of detail:

Code (prefix of detail) When
eth_authenticator_limit Hit the cap on wallets linked to one DID (e.g. trying to add an 11th)
eth_auth_map_conflict The wallet is already linked to a different DID — resolve via account merge
email_not_verified A profile upsert ran before the email OTP was verified
user_not_found The DID has no profile yet
legacy_field A pre-2.0.0 dispatch field was sent (e.g. otp_code on user-upsert)