Wocha Docs

Error Reference

Complete reference for Wocha API error codes and handling patterns

Customer API errors return a consistent JSON body:

{
  "error": "not_found",
  "message": "User not found",
  "request_id": "req_a1b2c3d4e5f6",
  "docs_url": "https://docs.wocha.ai/errors/not_found"
}

Use the error code for programmatic handling. The message field is human-readable and may change without notice.

Validation errors may also include an errors object mapping field names to message arrays. Rate-limited responses include retry_after (seconds).

API errors vs SDK auth errors

Two separate error systems cover different layers of the stack:

LayerPackageError typeHTTP statusdocs_url
Customer API@wocha/sdk, wocha, wocha-goTyped API errors (WochaAuthenticationError, PermissionError, …)Real HTTP status (401, 404, 429, …)Yes — links to pages below
End-user auth@wocha/react, @wocha/nextjsWochaAuthError with code propertyN/A (thrown client-side during OAuth)Documented under SDK auth errors

Management API errors include request_id for support. SDK auth errors occur during login, callback, token refresh, and ID token validation — handle them in UI routes and auth handlers, not in Customer API middleware.

Handling errors in your application

All three Management SDKs map HTTP status codes to typed exceptions. Use errorCode / Code to branch on specific codes within each type.

TypeScript (@wocha/sdk)

import {
  WochaClient,
  WochaAuthenticationError,
  GreetPermissionError,
  WochaNotFoundError,
  WochaValidationError,
  WochaRateLimitError,
  WochaConflictError,
  GreetServerError,
  isGreetError,
} from "@wocha/sdk";
 
const wocha = new WochaClient({
  tenant: process.env.WOCHA_TENANT!,
  apiKey: process.env.WOCHA_API_KEY!,
});
 
try {
  await wocha.users.get("usr_abc123");
} catch (err) {
  if (err instanceof WochaAuthenticationError) {
    // 401 — invalid_key, token_inactive, auth_failed, …
    console.error("Re-authenticate:", err.errorCode, err.requestId);
  } else if (err instanceof GreetPermissionError) {
    // 403 — permission_denied, insufficient_scope, quota_exceeded, …
    console.error("Access denied:", err.errorCode, err.docsUrl);
  } else if (err instanceof WochaNotFoundError) {
    // 404 — resource does not exist
    return null;
  } else if (err instanceof WochaValidationError) {
    // 400 / 422 — fix payload using err.fieldErrors
    console.error(err.fieldErrors);
  } else if (err instanceof WochaRateLimitError) {
    // 429 — wait err.retryAfter seconds before retrying
    await new Promise((r) => setTimeout(r, (err.retryAfter ?? 60) * 1000));
  } else if (err instanceof WochaConflictError) {
    // 409 — resource already exists
  } else if (err instanceof GreetServerError) {
    // 5xx or network — safe to retry with backoff
    console.error("Server error:", err.statusCode, err.requestId);
  } else if (isGreetError(err)) {
    console.error(err.errorCode, err.message);
  }
  throw err;
}

Python (wocha)

from wocha import WochaClient
from wocha.errors import (
    AuthenticationError,
    PermissionError,
    NotFoundError,
    ValidationError,
    RateLimitError,
    ConflictError,
    ServerError,
    is_greet_error,
)
import asyncio
 
wocha = WochaClient(tenant="acme", api_key="wocha_mgmt_...")
 
try:
    wocha.users.get("usr_abc123")
except AuthenticationError as e:
    # 401
    print(f"Re-authenticate: {e.error_code} ({e.request_id})")
except PermissionError as e:
    # 403
    print(f"Access denied: {e.error_code}")
except NotFoundError:
    # 404
    user = None
except ValidationError as e:
    # 400 / 422
    print(e.field_errors)
except RateLimitError as e:
    # 429
    asyncio.sleep(e.retry_after or 60)
except ConflictError as e:
    # 409
    print(f"Conflict: {e.message}")
except ServerError as e:
    # 5xx or network
    print(f"Server error: {e.status_code} ({e.request_id})")
except Exception as e:
    if is_greet_error(e):
        print(e.error_code, e.message)
    raise

Go (github.com/wocha-dev/wocha-go)

import (
    "errors"
    "time"
 
    wocha "github.com/wocha-dev/wocha-go"
)
 
client, _ := wocha.NewClient(
    wocha.WithTenant("acme"),
    wocha.WithAPIKey("wocha_mgmt_..."),
)
 
_, err := client.Users.Get(ctx, "usr_abc123")
if err != nil {
    var authErr *wocha.AuthenticationError
    var permErr *wocha.PermissionError
    var notFound *wocha.NotFoundError
    var validation *wocha.ValidationError
    var rateLimit *wocha.RateLimitError
    var conflict *wocha.ConflictError
    var server *wocha.ServerError
 
    switch {
    case errors.As(err, &authErr):
        // 401
        log.Printf("Re-authenticate: %s (%s)", authErr.Code, authErr.RequestID)
    case errors.As(err, &permErr):
        // 403
        log.Printf("Access denied: %s", permErr.Code)
    case errors.As(err, &notFound):
        // 404 — resource does not exist
        return nil, nil
    case errors.As(err, &validation):
        // 400 / 422
        log.Printf("Validation: %v", validation.FieldErrors)
    case errors.As(err, &rateLimit):
        // 429
        if rateLimit.RetryAfter != nil {
            time.Sleep(time.Duration(*rateLimit.RetryAfter) * time.Second)
        }
    case errors.As(err, &conflict):
        // 409
        log.Printf("Conflict: %s", conflict.Message)
    case errors.As(err, &server):
        // 5xx or network
        log.Printf("Server error: %d (%s)", server.StatusCode, server.RequestID)
    default:
        if wocha.IsAPIError(err) {
            var apiErr *wocha.APIError
            errors.As(err, &apiErr)
            log.Printf("%s: %s", apiErr.Code, apiErr.Message)
        }
    }
    return nil, err
}

Authentication and keys

These errors indicate the caller could not be authenticated. Do not retry without fixing credentials.

CodeHTTPDescriptionCommon causesResolution
unauthorized401Missing or malformed Authorization headerNo Bearer prefix; header omitted entirelyAdd Authorization: Bearer wocha_mgmt_… or a valid OAuth access token
invalid_key401Management API key rejectedWrong key, wrong tenant, revoked or expired key, rotation grace period endedCreate a new key in Console; verify WOCHA_TENANT matches the key's tenant
expired_key401Key past its expires_atKey created with expiry; rotation grace expiredRotate or create a replacement key; update secrets in your deployment
key_revoked401Key explicitly revokedKey deleted in Console; security rotationCreate a new key; audit logs show key_revoked in metadata
token_inactive401OAuth bearer token inactive or expiredAccess token expired; token revoked in HydraRefresh the token or re-run the OAuth flow
auth_failed401Authentication subsystem errorDatabase unavailable during key lookup; unexpected failureRetry once; if persistent, check platform status and contact support with request_id
introspection_failed401OAuth token introspection failedHydra unreachable; malformed bearer tokenVerify token format; check Hydra connectivity for self-hosted deployments

Example — authentication errors

TypeScript
import { WochaAuthenticationError } from "@wocha/sdk";
 
try {
  await wocha.apiKeys.list();
} catch (err) {
  if (err instanceof WochaAuthenticationError) {
    switch (err.errorCode) {
      case "invalid_key":
      case "expired_key":
      case "key_revoked":
        alertOps("Rotate management API key", err.requestId);
        break;
      case "token_inactive":
        await refreshOAuthToken();
        break;
      default:
        console.error(err.errorCode, err.docsUrl);
    }
  }
}
Python
from wocha.errors import AuthenticationError
 
try:
    wocha.api_keys.list()
except AuthenticationError as e:
    if e.error_code in ("invalid_key", "expired_key", "key_revoked"):
        alert_ops(f"Rotate key ({e.request_id})")
    elif e.error_code == "token_inactive":
        refresh_oauth_token()
    else:
        print(e.error_code, e.docs_url)
Go
_, err := client.APIKeys.List(ctx, nil)
var authErr *wocha.AuthenticationError
if errors.As(err, &authErr) {
    switch authErr.Code {
    case "invalid_key", "expired_key", "key_revoked":
        alertOps(authErr.RequestID)
    case "token_inactive":
        refreshOAuthToken()
    default:
        log.Println(authErr.Code, authErr.DocsURL)
    }
}

Authorisation, scopes, and quotas

These errors mean the caller is authenticated but not permitted to perform the action. Do not retry without changing scopes, plan tier, or permissions.

CodeHTTPDescriptionCommon causesResolution
permission_denied403Generic permission failureSpiceDB check denied; actor lacks resource accessVerify relationships via permissions.check() or Console
forbidden403Alias for insufficient accessSame as permission_deniedSee permission_denied
insufficient_scope403API key or token missing required scopeKey scoped to users:read calling users:writeAdd scope in Console → Settings → API keys
tenant_inactive403Tenant suspended or deactivatedBilling issue; manual suspensionContact support or resolve billing
feature_not_available403Feature not on current planAudit log export on Free tier; permission schema on StarterUpgrade plan or use an alternative endpoint
feature_unavailable403Alias for plan-gated featuresEntitlement check failedUpgrade plan
quota_exceeded403Plan resource quota reachedToo many applications, webhooks, or MAU capReview usage in Console → Billing; upgrade or delete unused resources

Example — authorisation errors

TypeScript
import { GreetPermissionError } from "@wocha/sdk";
 
try {
  await wocha.users.create({ email: "user@example.com" });
} catch (err) {
  if (err instanceof GreetPermissionError) {
    if (err.errorCode === "insufficient_scope") {
      console.error("Add users:write scope to your API key");
    } else if (err.errorCode === "quota_exceeded") {
      console.error("Plan quota reached — upgrade or remove unused resources");
    } else {
      console.error(err.errorCode, err.requestId);
    }
  }
}
Python
from wocha.errors import PermissionError
 
try:
    wocha.users.create(email="user@example.com")
except PermissionError as e:
    if e.error_code == "insufficient_scope":
        print("Add users:write scope to your API key")
    elif e.error_code == "quota_exceeded":
        print("Plan quota reached")
    else:
        print(e.error_code, e.request_id)
Go
_, err := client.Users.Create(ctx, &wocha.UserCreateParams{Email: "user@example.com"})
var permErr *wocha.PermissionError
if errors.As(err, &permErr) {
    switch permErr.Code {
    case "insufficient_scope":
        log.Println("Add users:write scope to your API key")
    case "quota_exceeded":
        log.Println("Plan quota reached")
    default:
        log.Println(permErr.Code, permErr.RequestID)
    }
}

Validation and client errors

These errors indicate a problem with the request itself. Fix the payload before retrying.

CodeHTTPDescriptionCommon causesResolution
validation_error400 / 422Field-level validation failedInvalid slug format; missing required field; value out of rangeRead errors / fieldErrors; compare against OpenAPI spec
invalid_request400Malformed requestInvalid JSON body; unsupported content typeVerify JSON syntax and Content-Type: application/json
missing_tenant400Tenant context unresolvedOAuth token without tenant claim; missing X-Tenant-IDPass X-Tenant-ID header or use a tenant-scoped token
billing_not_configured400Billing not set upCalling billing portal before Stripe configurationComplete billing setup in Console
conflict409Resource already existsDuplicate slug, email, or product identifierUse a unique identifier or fetch the existing resource
not_found404Resource does not existWrong ID; resource deleted; wrong tenantVerify ID from a recent list() response; handle 404 gracefully in read paths

Example — validation and client errors

TypeScript
import { WochaValidationError, WochaNotFoundError, WochaConflictError } from "@wocha/sdk";
 
try {
  await wocha.organisations.create({ slug: "engineering", display_name: "Engineering" });
} catch (err) {
  if (err instanceof WochaValidationError) {
    for (const [field, messages] of Object.entries(err.fieldErrors ?? {})) {
      setFieldError(field, messages[0]);
    }
  } else if (err instanceof WochaConflictError) {
    const { data } = await wocha.organisations.list();
    const existing = data.find((o) => o.slug === "engineering");
    if (existing) return existing;
  } else if (err instanceof WochaNotFoundError) {
    return null;
  }
  throw err;
}
Python
from wocha.errors import ValidationError, ConflictError, NotFoundError
 
try:
    wocha.organisations.create(slug="engineering", display_name="Engineering")
except ValidationError as e:
    for field, messages in (e.field_errors or {}).items():
        set_field_error(field, messages[0])
except ConflictError:
    for org in wocha.organisations.list_all():
        if org["slug"] == "engineering":
            return org
except NotFoundError:
    return None
Go
org, err := client.Organisations.Create(ctx, &wocha.OrganisationCreateParams{
    Slug: "engineering", DisplayName: "Engineering",
})
var validation *wocha.ValidationError
var conflict *wocha.ConflictError
var notFound *wocha.NotFoundError
switch {
case errors.As(err, &validation):
    for field, msgs := range validation.FieldErrors {
        setFieldError(field, msgs[0])
    }
case errors.As(err, &conflict):
    orgs, _ := client.Organisations.ListAll(ctx, nil)
    for _, o := range orgs {
        if o.Slug == "engineering" {
            return &o, nil
        }
    }
case errors.As(err, &notFound):
    return nil, nil
}

Rate limiting

CodeHTTPDescriptionCommon causesResolution
rate_limit_exceeded429Per-minute request limit exceeded (canonical code)Polling loops; bulk scripts without delays; shared key across servicesRespect Retry-After; enable SDK auto-retry; reduce request volume
rate_limited429Legacy alias for rate limitingSame as rate_limit_exceededSame as above

When rate limited, responses include:

HeaderDescription
X-RateLimit-LimitMaximum requests allowed in the window
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix timestamp when the window resets
Retry-AfterSeconds to wait before retrying (429 only)

Rate limit tiers

TierRate limit
Free100 req/min
Starter500 req/min
Pro2,000 req/min
Enterprise10,000 req/min

Example — rate limiting

TypeScript
import { WochaRateLimitError } from "@wocha/sdk";
 
try {
  await wocha.logs.list({ page_size: 50 });
} catch (err) {
  if (err instanceof WochaRateLimitError) {
    const delay = (err.retryAfter ?? 60) * 1000;
    await new Promise((r) => setTimeout(r, delay));
    return wocha.logs.list({ page_size: 50 });
  }
  throw err;
}
Python
import time
from wocha.errors import RateLimitError
 
try:
    wocha.logs.list(page_size=50)
except RateLimitError as e:
    time.sleep(e.retry_after or 60)
    wocha.logs.list(page_size=50)
Go
logs, err := client.Logs.List(ctx, &wocha.ListParams{PageSize: wocha.Int(50)})
var rateLimit *wocha.RateLimitError
if errors.As(err, &rateLimit) {
    delay := 60
    if rateLimit.RetryAfter != nil {
        delay = *rateLimit.RetryAfter
    }
    time.Sleep(time.Duration(delay) * time.Second)
    logs, err = client.Logs.List(ctx, &wocha.ListParams{PageSize: wocha.Int(50)})
}

Note: The SDK does not retry quota_exceeded (403) — only transient 429/5xx responses.


Server and upstream errors

These errors indicate a platform or dependency failure. Safe to retry with exponential backoff.

CodeHTTPDescriptionCommon causesResolution
internal_error500Unhandled server exceptionUnexpected bug; database timeoutRetry with backoff; contact support with request_id if persistent
server_error500Generic server failureSame as internal_errorSame as above
upstream_error502Upstream dependency failedKratos, Hydra, or SpiceDB returned an errorRetry with backoff; check self-hosted service health
service_unavailable503Required service not configured or downSpiceDB not configured; billing service offlineVerify service configuration; check platform status
unknown_errorvariesUnrecognised error codeNew API error not yet in SDK; proxy interferenceLog full response; check SDK version; contact support

Example — server errors

TypeScript
import { GreetServerError } from "@wocha/sdk";
 
async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (err) {
      if (err instanceof GreetServerError && err.statusCode >= 500 && i < attempts - 1) {
        await new Promise((r) => setTimeout(r, 1000 * 2 ** i));
        continue;
      }
      throw err;
    }
  }
  throw new Error("unreachable");
}
Python
import time
from wocha.errors import ServerError
 
def with_retry(fn, attempts=3):
    for i in range(attempts):
        try:
            return fn()
        except ServerError as e:
            if e.status_code >= 500 and i < attempts - 1:
                time.sleep(2 ** i)
                continue
            raise
Go
func withRetry[T any](attempts int, fn func() (T, error)) (T, error) {
    var zero T
    var lastErr error
    for i := 0; i < attempts; i++ {
        result, err := fn()
        if err == nil {
            return result, nil
        }
        var server *wocha.ServerError
        if errors.As(err, &server) && server.StatusCode >= 500 && i < attempts-1 {
            time.Sleep(time.Duration(1<<i) * time.Second)
            lastErr = err
            continue
        }
        return zero, err
    }
    return zero, lastErr
}

SDK auth errors

These are thrown by @wocha/react and @wocha/nextjs during end-user authentication — not returned as Customer API JSON.

CodeDescriptionCommon causesResolution
state_mismatchOAuth state parameter mismatchCSRF check failed; session cookie lost; multiple tabsRestart login flow; ensure cookies persist across redirect
token_exchange_failedAuthorization code exchange failedInvalid code; redirect URI mismatch; expired codeVerify redirect URI in Console; retry login
token_refresh_failedRefresh token rejectedRefresh token expired or revokedForce re-login
missing_verifierPKCE code verifier missingSession storage cleared between redirect stepsEnsure PKCE verifier stored in same session/cookie
network_errorNetwork failure during authOffline client; DNS failure; timeoutRetry; check connectivity
auth_failedGeneric auth failureProvider error; misconfigured clientCheck Console OAuth app settings
invalid_id_tokenID token validation failedWrong issuer, audience, expiry, or nonceVerify client ID and issuer URL

Next.js also defines: id_token_issuer_mismatch, id_token_audience_mismatch, id_token_expired, oidc_discovery_failed, jwks_fetch_failed, invalid_nonce — see invalid_id_token and network_error.

Example — SDK auth errors (React / Next.js)

import { WochaAuthError } from "@wocha/react";
 
try {
  await signIn();
} catch (err) {
  if (err instanceof WochaAuthError) {
    switch (err.code) {
      case "state_mismatch":
        window.location.href = "/login";
        break;
      case "token_refresh_failed":
        await signOut();
        break;
      case "network_error":
        showToast("Connection lost — please try again");
        break;
      default:
        console.error(err.code, err.message);
    }
  }
}

WochaValidationError.fieldErrors

When the API returns validation_error, the JSON body may include an errors object:

{
  "error": "validation_error",
  "message": "Validation failed",
  "errors": {
    "slug": ["must be lowercase alphanumeric with hyphens"],
    "email": ["is required", "must be a valid email address"]
  },
  "request_id": "req_xyz"
}

The TypeScript SDK maps this to WochaValidationError.fieldErrors:

import { WochaValidationError } from "@wocha/sdk";
 
try {
  await wocha.users.create({ email: "not-an-email" });
} catch (err) {
  if (err instanceof WochaValidationError && err.fieldErrors) {
    for (const [field, messages] of Object.entries(err.fieldErrors)) {
      setFieldError(field, messages[0]);
    }
  }
}

Properties:

  • fieldErrorsRecord<string, string[]> | undefined. Keys are API field names; values are one or more human-readable messages per field.
  • statusCode400 or 422 depending on the route.
  • errorCode — always "validation_error" for this class.

Use fieldErrors for inline form validation. Use message for a generic summary banner.


Quick reference index

CategoryCodes
Authenticationunauthorized, invalid_key, expired_key, key_revoked, token_inactive, auth_failed, introspection_failed
Authorisationpermission_denied, forbidden, insufficient_scope, tenant_inactive, feature_not_available, feature_unavailable, quota_exceeded
Clientvalidation_error, invalid_request, missing_tenant, billing_not_configured, conflict, not_found
Rate limitingrate_limit_exceeded, rate_limited
Serverinternal_error, server_error, upstream_error, service_unavailable, unknown_error
SDK authstate_mismatch, token_exchange_failed, token_refresh_failed, missing_verifier, network_error, auth_failed, invalid_id_token

Individual error pages with additional context: