Skip to main content

Developer API errors

All Developer API errors return a JSON object with an error code and a human-readable message:

Public auth errors (SDK)

The public auth endpoints (/auth/validate, /auth/heartbeat, and /auth/selfban) return errors as real HTTP status codes with a JSON body of the form {"status": "failed", "error": "<code>"}:
The HTTP status code matches the class of failure: Successful responses return 200 with status: "success". Public auth requests are protected by two layers of throttling:
  • API Gateway stage throttling: burst 200, sustained 100 req/s (default for every route on this API: /auth/validate, /auth/heartbeat, and /auth/selfban)
  • Application-level per-minute limits on /auth/validate:
    • 30/min per IP
    • 5/min per license key
  • Application-level per-minute limit on /auth/heartbeat:
    • 6/min per license key
/auth/heartbeat is not IP rate-limited at the application layer, only per-license. The default online check-in interval of 15 minutes is far below the limit; the per-license cap exists to stop runaway loops. Unlimited-seat (shared) licenses are limited per device instead of per key, so a popular shared key is not throttled as one bucket: 5/min per device on /auth/validate and 6/min per device on /auth/heartbeat (a device is the license key plus its HWID). Each endpoint also has a ceiling across all devices on the key: 300/min on /auth/validate and 3000/min on /auth/heartbeat. Validate’s is lower because the client chooses the HWID and every success costs a credit, so the per-key ceiling is what bounds a leaked key. These limits use fixed one-minute windows, so a launch rejected by the 300/min validate ceiling will usually be rejected again if retried within the same minute. Apps that run one shared key across a large fleet that may start all at once should retry login() on rate_limited with exponential backoff for up to about 60 seconds. Additional application limits on /auth/selfban: 10/min per IP (all self-ban requests), and 3/min per license key for pre-session requests that send licenseKey + app credentials. When a validate request exceeds either limit, the response is HTTP 429 with:
Successful /auth/validate responses include the X-RateLimit-Remaining header (the lowest of the remaining IP and per-license validate quotas; for shared keys, the per-device and per-key quotas). Successful /auth/heartbeat responses include the same header with the remaining per-license check-in quota (for shared keys, the lower of the per-device and per-key quotas). The official SDKs parse both the HTTP status code and the JSON error field, so 429 responses surface as rate_limited automatically; no special handling is needed in your integration.

Validate errors (/auth/validate)

Heartbeat errors (/auth/heartbeat)

The SDK classification column shows what the official SDKs (1.4.0+) do when a check-in fails with each code:
  • Definitive: the SDK clears the stored session (as logout() does) and stops check-ins, then calls onFailure("heartbeat_failed", error).
  • Transient: the SDK keeps the session, calls onFailure("heartbeat_failed", error), and checks in again on the next interval. Once the session TTL has passed, the next transient failure is reported as a definitive session_expired. The SDKs report the grace period ending the same way, except Rust, which reports local session expiry as expired (AuthForgeError::Expired) until its next major release.
A code is only treated as an AuthForge verdict when it arrives in a well-formed error body ({"status": "failed", "error": "<code>"}); anything else is reported as a transient SDK error (see SDK client-side errors). SDKs 1.4.0+ expose the code and classification on the error passed to onFailure; see Check-in and session failure handling for the per-language API. invalid_app is also definitive in the SDKs, but /auth/heartbeat does not return it: a deactivated app yields session_expired.
replay_detected is never returned from /auth/heartbeat. Heartbeats do not enforce nonce replay detection; replay protection is provided by the signed, short-lived session token. Heartbeat rate limiting is per license (6/min; per device for unlimited-seat licenses), never per IP.

Self-ban errors (/auth/selfban)


SDK client-side errors

In addition to server errors, the SDK may detect issues locally before or after a server response: These errors trigger the onFailure callback with the "login_failed" or "heartbeat_failed" reason. Any code an SDK version does not recognize is passed through unchanged and classified as transient.

Offline license file errors (loginFromFile / verifyLicenseFile)

Verifying a .authforge offline license file is entirely local. Every SDK runs the same checks in this order and reports the first failure through onFailure("offline_login_failed", error) (Go and Rust return typed errors instead): loginFromFile never terminates the process on its own; display the message and exit from your code.

Best practices for error handling

  1. Never expose internal error codes to end users. Map them to user-friendly messages.
  2. Log the actual error code for debugging, but show generic messages to users.
  3. Let the SDK handle transient check-in errors. Network errors, rate_limited, system_error, no_credits, app_burn_cap_reached, and the other transient codes keep the session, and the SDK checks in again on the next interval. It retries rate_limited (after 2 and 5 seconds) and network failures (once) internally, but deliberately waits for the next interval on no_credits, demo_quota_exceeded, and app_burn_cap_reached; don’t add your own retry loop on top. For login(), retrying network failures a few times with backoff is fine; retrying no_credits is not, since it only clears once you add credits.
  4. Don’t retry definitive errors: revoked, expired, hwid_mismatch, blocked, session_expired, malformed_request, app_disabled, invalid_app, and signature_mismatch (at login, invalid_key is also final for the key the user entered). On a check-in, the SDK has already cleared the session; prompt the user to take action instead.
  5. Handle no_credits gracefully: this is a billing issue on your end, not the user’s problem.
See SDK Best Practices for detailed error handling guidance with code examples.