API Reference
Error Codes
Complete reference of all HelloJohn API error codes, their meanings, and how to resolve them.
Error Codes
All API errors return a JSON body with a structured error object:
{
"error": {
"code": "invalid_credentials",
"message": "The email or password is incorrect.",
"status": 401,
"request_id": "req_01HABCDEF999888"
}
}Use code (not status or message) for programmatic error handling — messages may change between versions.
HTTP Status Summary
| Status | Meaning |
|---|---|
200 | Success |
201 | Created |
204 | Success, no content |
400 | Bad request — invalid input |
401 | Unauthenticated — missing or invalid token |
403 | Forbidden — valid token, insufficient permissions |
404 | Resource not found |
409 | Conflict — resource already exists |
422 | Unprocessable entity — validation failed |
429 | Too many requests — rate limit exceeded |
500 | Internal server error |
503 | Service unavailable |
Authentication Errors (401)
| Code | Status | Description | Resolution |
|---|---|---|---|
missing_token | 401 | No Authorization header present | Include Authorization: Bearer <token> |
invalid_token | 401 | Token is malformed or has invalid signature | Re-authenticate to get a fresh token |
token_expired | 401 | Access token has expired | Use refresh token to get a new access token |
refresh_token_expired | 401 | Refresh token has expired (session ended) | User must sign in again |
refresh_token_invalid | 401 | Refresh token does not match session | Re-authenticate |
invalid_api_key | 401 | Secret key sk_... is invalid or revoked | Regenerate the key in dashboard |
invalid_publishable_key | 401 | Publishable key pk_... is invalid | Check the key in dashboard settings |
session_revoked | 401 | Session was explicitly revoked | User must sign in again |
Authorization Errors (403)
| Code | Status | Description | Resolution |
|---|---|---|---|
insufficient_permissions | 403 | Token is valid but lacks required role/permission | Use a token with higher permissions |
tenant_mismatch | 403 | Resource belongs to a different tenant | Verify X-Tenant-ID header |
cp_admin_required | 403 | Endpoint requires a CP Admin token | Use a Control Plane admin secret key |
mfa_required | 403 | MFA verification required before proceeding | Complete MFA challenge first |
email_not_verified | 403 | User's email must be verified first | Trigger email verification flow |
account_disabled | 403 | User account has been disabled | Contact tenant admin |
account_locked | 403 | Account temporarily locked (too many failed attempts) | Wait for lockout period or reset password |
org_member_required | 403 | Action requires org membership | Join the organization first |
org_role_insufficient | 403 | Action requires a higher org role | Requires admin or owner role |
Validation Errors (400 / 422)
| Code | Status | Description | Resolution |
|---|---|---|---|
invalid_request | 400 | Request body is malformed JSON | Check request body format |
missing_required_field | 422 | A required field is absent | Include all required fields |
invalid_email | 422 | Email format is invalid | Provide a valid email address |
password_too_short | 422 | Password does not meet minimum length | Use at least 8 characters |
password_too_weak | 422 | Password failed strength check (zxcvbn) | Use a stronger password |
password_breached | 422 | Password found in breach database (HIBP) | Choose a different password |
invalid_phone | 422 | Phone number format is invalid | Use E.164 format (e.g., +14155552671) |
invalid_cursor | 400 | Pagination cursor is malformed | Use the exact cursor from the previous response |
invalid_date | 422 | Date field is not a valid ISO 8601 string | Use format 2024-01-15T10:30:00Z |
field_too_long | 422 | A field exceeds maximum length | Check field length limits |
unsupported_oauth_provider | 400 | OAuth provider is not configured for this tenant | Enable the provider in tenant settings |
Resource Errors (404 / 409)
| Code | Status | Description | Resolution |
|---|---|---|---|
user_not_found | 404 | No user matches the given ID or email | Verify the user ID or email |
session_not_found | 404 | Session does not exist | Session may have expired or been revoked |
org_not_found | 404 | Organization not found | Verify the org ID |
tenant_not_found | 404 | Tenant does not exist | Verify the tenant ID or slug |
client_not_found | 404 | Client (app) does not exist | Verify the client ID |
mfa_factor_not_found | 404 | MFA factor not found | User may not have enrolled this factor |
webhook_not_found | 404 | Webhook endpoint not found | Verify the webhook ID |
api_key_not_found | 404 | API key not found | Key may have been revoked |
invitation_not_found | 404 | Org invitation not found or expired | Re-send the invitation |
email_already_exists | 409 | Email is already registered | Sign in or use password reset |
username_already_exists | 409 | Username is already taken | Choose a different username |
mfa_factor_already_enrolled | 409 | User already has this MFA factor enrolled | Unenroll first to re-enroll |
org_name_already_exists | 409 | Organization name is taken within tenant | Choose a different org name |
member_already_in_org | 409 | User is already a member of this org | No action needed |
Authentication Flow Errors
| Code | Status | Description | Resolution |
|---|---|---|---|
invalid_credentials | 401 | Email or password is incorrect | Check credentials |
invalid_magic_link | 401 | Magic link token is invalid or expired | Request a new magic link |
invalid_otp | 401 | OTP code is incorrect | Re-enter the code or request a new one |
otp_expired | 401 | OTP has expired | Request a new OTP |
invalid_totp | 401 | TOTP code is invalid | Check device clock sync and try again |
backup_code_invalid | 401 | Backup code is invalid or already used | Use a different backup code |
oauth_state_mismatch | 400 | OAuth state parameter does not match | Restart OAuth flow |
oauth_denied | 400 | User denied OAuth authorization | User cancelled — no action required |
oauth_error | 400 | OAuth provider returned an error | Check provider configuration |
saml_assertion_invalid | 401 | SAML assertion failed validation | Check IdP configuration |
Rate Limit Errors (429)
| Code | Status | Description | Resolution |
|---|---|---|---|
rate_limit_exceeded | 429 | Too many requests in the time window | Wait for Retry-After seconds |
account_lockout | 429 | Too many failed sign-in attempts | Wait for the lockout period |
The Retry-After header contains the number of seconds to wait:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Reset: 1700000060Server Errors (5xx)
| Code | Status | Description | Resolution |
|---|---|---|---|
internal_server_error | 500 | Unexpected server error | Retry with exponential backoff; contact support if persistent |
database_error | 500 | Database operation failed | Retry; check database health |
service_unavailable | 503 | Server is temporarily unavailable | Retry after a few seconds |
migration_in_progress | 503 | Server is applying schema migrations | Wait 30-60 seconds and retry |
Handling Errors: Examples
TypeScript / Node.js:
try {
const user = await hellojohn.users.get(userId)
} catch (err) {
if (err instanceof HelloJohnError) {
switch (err.code) {
case 'user_not_found':
return res.status(404).json({ message: 'User does not exist' })
case 'token_expired':
return res.status(401).json({ message: 'Please sign in again' })
default:
console.error('HelloJohn error:', err.code, err.message)
return res.status(500).json({ message: 'Internal error' })
}
}
}Go:
user, err := client.Users().Get(ctx, userID)
if err != nil {
var apiErr *hj.APIError
if errors.As(err, &apiErr) {
switch apiErr.Code {
case "user_not_found":
http.Error(w, "user not found", http.StatusNotFound)
case "token_expired":
http.Error(w, "session expired", http.StatusUnauthorized)
default:
http.Error(w, "internal error", http.StatusInternalServerError)
}
return
}
}Python:
from hellojohn.exceptions import HelloJohnError
try:
user = await client.users.get(user_id)
except HelloJohnError as e:
if e.code == "user_not_found":
raise HTTPException(status_code=404, detail="User not found")
elif e.code == "token_expired":
raise HTTPException(status_code=401, detail="Session expired")
raise