Authentication
Section titled “Authentication”The Hoody API authenticates users with short-lived JWT access tokens (1 day) paired with refresh tokens (7 days), supports email/password and OAuth (GitHub, Google) flows, and exposes end-to-end two-factor authentication with TOTP and backup codes. Use this page to sign users up, log them in, refresh sessions, drive OAuth and RFC 8628 device flows, and configure 2FA.
Authenticated endpoints accept a JWT in the Authorization header: Authorization: Bearer <token>. Responses from endpoints that support signing carry an X-Hoody-Signature header you can verify against the public key returned by GET /api/v1/meta/public-key.
Server discovery
Section titled “Server discovery”GET /api/v1/auth/available-regions
Section titled “GET /api/v1/auth/available-regions”Returns regions where free-tier servers exist, with a boolean availability flag. Public, no authentication required.
This endpoint takes no parameters.
curl -X GET https://api.hoody.com/api/v1/auth/available-regionsconst result = await client.api.authentication.getAvailableRegions();{ "statusCode": 200, "data": { "regions": [ { "region": "eu-west", "country": "FR", "city": "Paris", "available": true }, { "region": "us-east", "country": "US", "city": "New York", "available": false } ] }}GET /api/v1/meta/public-key
Section titled “GET /api/v1/meta/public-key”Returns the ED25519 public key(s) used by Hoody to sign API responses (X-Hoody-Signature header), identity claims issued at login, and container authorization claims. Public, no authentication required. Cache the result for at least 24 hours.
This endpoint takes no parameters.
curl -X GET https://api.hoody.com/api/v1/meta/public-keyconst result = await client.api.meta.getPublicKey();{ "statusCode": 200, "message": "Hoody API signing public key", "data": { "keys": [ { "kid": "v1", "algorithm": "ed25519", "public_key_hex": "8c8d683c125761bd9157e3a6f98c30d81cd7f2be4d16062a8342d1fcd2ca474a", "public_key_b64": "jI1oPBJXYb2RV+Om+YwwwlzX8r5NFgYqg0LRzSykd0o=", "public_key_b64url": "jI1oPBJXYb2RV-Om-YwwwlzX8r5NFgYqg0LRzSykd0o" } ], "active_kid": "v1", "usage": ["response-signing", "identity-claims", "container-claims"], "signing_format": { "response_header": "X-Hoody-Signature: t=<unix_ts>,kid=<key_id>,path=<request_url>,sig=<hex>", "response_signed_data": "<t_value>.<response_body_utf8_string>", "identity_claim_signed_data": "base64url(JSON.stringify(claim_payload)) — the b64url string itself (UTF-8 bytes)", "container_claim_signed_data": "base64url(JSON.stringify(container_claim_payload)) — the b64url string itself (UTF-8 bytes)", "replay_tolerance_seconds": 300 } }}{ "statusCode": 503, "error": "SIGNING_NOT_CONFIGURED", "message": "Response signing is not configured on this API instance"}Email & password
Section titled “Email & password”POST /api/v1/auth/signup
Section titled “POST /api/v1/auth/signup”Create a new account with email and password. A verification email is sent; the account is inactive until the email is verified.
This endpoint takes no parameters.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Email address for the new account |
password | string | Yes | Password (min 12 chars, must include uppercase, lowercase, number, and special char) |
region | string | No | Preferred server region (e.g. eu-west). Auto-assigned by GeoIP if omitted |
curl -X POST https://api.hoody.com/api/v1/auth/signup \ -H "Content-Type: application/json" \ -d '{ "email": "alice@example.com", "password": "S3cure!Passw0rd", "region": "eu-west" }'const result = await client.api.authentication.signup({ email: "alice@example.com", password: "S3cure!Passw0rd", region: "eu-west"});{ "statusCode": 200, "message": "Verification email sent", "data": { "email": "alice@example.com" }}{ "statusCode": 400, "message": "Password must be at least 12 characters and include uppercase, lowercase, number, and special character"}{ "statusCode": 403, "message": "Signups are currently disabled"}POST /api/v1/auth/verify-email
Section titled “POST /api/v1/auth/verify-email”Verify the email address using the token from the verification email. By default returns full login credentials. When response_mode=intent and code_challenge are provided, returns an opaque auth_intent_token for PKCE exchange. If 2FA is enabled on the account, returns requires_2fa and temp_token instead.
This endpoint takes no parameters.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes | Verification token from the email link (64 chars) |
response_mode | string | No | tokens (default) or intent |
code_challenge | string | No | PKCE code_challenge. Required when response_mode=intent |
curl -X POST https://api.hoody.com/api/v1/auth/verify-email \ -H "Content-Type: application/json" \ -d '{ "token": "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234" }'const result = await client.api.authentication.verifyEmail({ token: "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234"});{ "statusCode": 200, "message": "Email verified", "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expires_at": "2025-11-28T20:19:00.000Z", "expires_in": 86400, "refresh_expires_at": "2025-12-04T20:19:00.000Z", "refresh_expires_in": 604800, "method": "totp", "user": { "id": "507f1f77bcf86cd799439011", "username": "alice", "alias": "Alice", "email": "alice@example.com", "is_admin": false, "email_verified": true, "signup_method": "email", "avatar_url": null, "created_at": "2025-11-27T20:19:00.000Z", "updated_at": "2025-11-27T20:19:00.000Z" } }}{ "statusCode": 400, "message": "Invalid or expired verification token"}POST /api/v1/auth/resend-verification
Section titled “POST /api/v1/auth/resend-verification”Resend the email verification link. Always returns success to prevent email enumeration.
This endpoint takes no parameters.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Email address to resend verification to |
curl -X POST https://api.hoody.com/api/v1/auth/resend-verification \ -H "Content-Type: application/json" \ -d '{ "email": "alice@example.com" }'const result = await client.api.authentication.resendVerification({ email: "alice@example.com"});{ "statusCode": 200, "message": "If an unverified account exists for that email, a verification link has been sent"}POST /api/v1/auth/forgot-password
Section titled “POST /api/v1/auth/forgot-password”Send a password reset email. Always returns success to prevent email enumeration.
This endpoint takes no parameters.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Email address associated with the account |
curl -X POST https://api.hoody.com/api/v1/auth/forgot-password \ -H "Content-Type: application/json" \ -d '{ "email": "alice@example.com" }'const result = await client.api.authentication.forgotPassword({ email: "alice@example.com"});{ "statusCode": 200, "message": "If an account exists for that email, a password reset link has been sent"}POST /api/v1/auth/reset-password
Section titled “POST /api/v1/auth/reset-password”Set a new password using the reset token from the password reset email.
This endpoint takes no parameters.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes | Password reset token from the email link (64 chars) |
password | string | Yes | New password (min 12 chars) |
curl -X POST https://api.hoody.com/api/v1/auth/reset-password \ -H "Content-Type: application/json" \ -d '{ "token": "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234", "password": "N3wS3cure!Passw0rd" }'const result = await client.api.authentication.resetPassword({ token: "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234", password: "N3wS3cure!Passw0rd"});{ "statusCode": 200, "message": "Password reset successful"}{ "statusCode": 400, "message": "Invalid or expired reset token"}Login & sessions
Section titled “Login & sessions”POST /api/v1/users/auth/login
Section titled “POST /api/v1/users/auth/login”Authenticate with username (or email) and password to receive a JWT access token (1 day) and a refresh token (7 days). Use the access token in the Authorization header for subsequent requests.
This endpoint takes no parameters.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
username | string | No | Username (alphanumeric, underscores, hyphens). Either username or email is required |
email | string | No | Email address. Either username or email is required |
password | string | Yes | Account password (min 8 chars, must include uppercase, lowercase, and number) |
response_mode | string | No | tokens (default) or intent (hosted auth UI PKCE flow) |
code_challenge | string | No | PKCE code_challenge. Required when response_mode=intent |
curl -X POST https://api.hoody.com/api/v1/users/auth/login \ -H "Content-Type: application/json" \ -d '{ "username": "john_doe", "password": "SecurePassword123!" }'const result = await client.api.authentication.login({ username: "john_doe", password: "SecurePassword123!"});{ "statusCode": 200, "message": "Login successful", "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expires_at": "2025-11-28T20:19:00.000Z", "expires_in": 86400, "refresh_expires_at": "2025-12-04T20:19:00.000Z", "refresh_expires_in": 604800, "client_ip": "192.168.1.100", "recent_login_ips": [ { "ip": "192.168.1.100", "timestamp": "2025-01-15T10:30:00.000Z" }, { "ip": "10.0.0.5", "timestamp": "2025-01-14T09:15:00.000Z" } ], "auth_token_count": 2, "user": { "id": "507f1f77bcf86cd799439011", "username": "john_doe", "alias": "John Doe", "email": "john.doe@example.com", "is_admin": false, "is_banned": false, "metadata": {}, "created_at": "2024-12-01T10:00:00.000Z", "updated_at": "2025-01-15T10:30:00.000Z" } }}{ "statusCode": 400, "error": "VALIDATION_ERROR", "message": "Validation failed"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Invalid input parameters | One or more request parameters failed validation | Check the error message for specific field requirements and correct your input |
MISSING_REQUIRED_FIELD | Required field missing | One or more required fields are missing from the request | Include all required fields as specified in the API documentation |
{ "statusCode": 401, "error": "INVALID_CREDENTIALS", "message": "Invalid email or password"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_CREDENTIALS | Invalid credentials | The provided username/email or password is incorrect | Verify your credentials and try again, or use the password reset feature |
EMAIL_NOT_VERIFIED | Email not verified | Returned by the login endpoint when the password is correct but the account’s email address has not been verified yet | Complete email verification by clicking the link sent to your inbox, or call /auth/resend-verification |
POST /api/v1/users/auth/logout
Section titled “POST /api/v1/users/auth/logout”Log out the current user. Creates an audit log entry. In a stateless JWT setup, the client should also discard the token. This endpoint works even for banned users.
This endpoint takes no parameters.
curl -X POST https://api.hoody.com/api/v1/users/auth/logout \ -H "Authorization: Bearer <token>"const result = await client.api.authentication.logout();{ "statusCode": 200, "message": "Logout successful"}{ "statusCode": 401, "error": "Unauthorized", "message": "Authentication token required"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TOKEN | Authentication token missing | No authentication token was provided in the request | Include a valid JWT token in the Authorization header as Bearer <token> |
INVALID_TOKEN | Invalid authentication token | The provided authentication token is malformed or invalid | Obtain a new token by logging in again or using a valid auth token |
TOKEN_EXPIRED | Authentication token expired | The provided authentication token has expired | Obtain a new token by logging in again or refreshing your session |
POST /api/v1/users/auth/refresh
Section titled “POST /api/v1/users/auth/refresh”Exchange a valid refresh token for a new access token and a new refresh token. Send the refresh token in the request body.
This endpoint takes no parameters.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
refreshToken | string | Yes | Valid refresh token from a previous login/refresh |
curl -X POST https://api.hoody.com/api/v1/users/auth/refresh \ -H "Content-Type: application/json" \ -d '{ "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }'const result = await client.api.authentication.refreshToken({ refreshToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."});{ "statusCode": 200, "message": "Token refreshed successfully", "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expires_at": "2025-11-28T20:19:00.000Z", "expires_in": 86400, "refresh_expires_at": "2025-12-04T20:19:00.000Z", "refresh_expires_in": 604800 }}{ "statusCode": 401, "error": "Unauthorized", "message": "Invalid refresh token"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TOKEN | Authentication token missing | No authentication token was provided in the request | Include a valid JWT token in the Authorization header as Bearer <token> |
INVALID_TOKEN | Invalid authentication token | The provided authentication token is malformed or invalid | Obtain a new token by logging in again or using a valid auth token |
TOKEN_EXPIRED | Authentication token expired | The provided authentication token has expired | Obtain a new token by logging in again or refreshing your session |
GET /api/v1/users/auth/me
Section titled “GET /api/v1/users/auth/me”Retrieve the profile of the currently authenticated user. Works with JWT, auth token, or Basic authentication. When authenticated with an auth token, the response includes data.auth_token introspection details (permissions and realm restrictions). This endpoint works even for banned users (read-only access).
This endpoint takes no parameters.
curl -X GET https://api.hoody.com/api/v1/users/auth/me \ -H "Authorization: Bearer <token>"const result = await client.api.authentication.getCurrentUser();{ "statusCode": 200, "message": "Current user retrieved successfully", "data": { "id": "507f1f77bcf86cd799439011", "username": "john_doe", "alias": "John Doe", "email": "john.doe@example.com", "is_admin": false, "is_banned": false, "metadata": {}, "created_at": "2024-12-01T10:00:00.000Z", "updated_at": "2025-01-15T10:30:00.000Z" }}{ "statusCode": 401, "error": "Unauthorized", "message": "Authentication token required"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TOKEN | Authentication token missing | No authentication token was provided in the request | Include a valid JWT token in the Authorization header as Bearer <token> |
INVALID_TOKEN | Invalid authentication token | The provided authentication token is malformed or invalid | Obtain a new token by logging in again or using a valid auth token |
TOKEN_EXPIRED | Authentication token expired | The provided authentication token has expired | Obtain a new token by logging in again or refreshing your session |
OAuth flows
Section titled “OAuth flows”GET /api/v1/auth/github
Section titled “GET /api/v1/auth/github”Redirects the browser to GitHub for OAuth authentication. Browser-only endpoint. Supports a star_check intent to check whether the user has starred the repository for credit.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
intent | query | string | No | OAuth intent: login (default) or star_check (check for star credit) |
redirect_uri | query | string | No | Frontend URL to redirect to after OAuth completes (must be on allowed domain) |
code_challenge | query | string | Yes | PKCE code_challenge (base64url SHA-256 of code_verifier). Required — all OAuth flows must use PKCE post-migration |
curl -X GET "https://api.hoody.com/api/v1/auth/github?intent=login&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"const result = await client.api.authentication.githubOAuthRedirect();This endpoint responds with an HTTP redirect (302) to the GitHub OAuth provider. The browser must follow the redirect.
GET /api/v1/auth/github/callback
Section titled “GET /api/v1/auth/github/callback”Handles the GitHub OAuth callback. Browser-only endpoint.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
code | query | string | Yes | Authorization code returned by GitHub |
state | query | string | Yes | State parameter for CSRF protection |
curl -X GET "https://api.hoody.com/api/v1/auth/github/callback?code=acbd1234&state=xyz987"const result = await client.api.authentication.githubOAuthCallback();This endpoint responds with an HTTP redirect (302) to the frontend after the OAuth flow completes.
GET /api/v1/auth/google
Section titled “GET /api/v1/auth/google”Redirects the browser to Google for OAuth authentication. Browser-only endpoint.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
redirect_uri | query | string | No | Frontend URL to redirect to after OAuth completes |
code_challenge | query | string | Yes | PKCE code_challenge (base64url SHA-256 of code_verifier). Required — all OAuth flows must use PKCE post-migration |
curl -X GET "https://api.hoody.com/api/v1/auth/google?code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"const result = await client.api.authentication.googleOAuthRedirect();This endpoint responds with an HTTP redirect (302) to the Google OAuth provider. The browser must follow the redirect.
GET /api/v1/auth/google/callback
Section titled “GET /api/v1/auth/google/callback”Handles the Google OAuth callback. Browser-only endpoint.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
code | query | string | Yes | Authorization code returned by Google |
state | query | string | Yes | State parameter for CSRF protection |
curl -X GET "https://api.hoody.com/api/v1/auth/google/callback?code=acbd1234&state=xyz987"const result = await client.api.authentication.googleOAuthCallback();This endpoint responds with an HTTP redirect (302) to the frontend after the OAuth flow completes.
POST /api/v1/auth/launch/initiate
Section titled “POST /api/v1/auth/launch/initiate”Initiate the OAuth popup-handoff launch flow. Issues a one-shot launch ticket bound to the request Origin header. The frontend navigates the popup to the returned launch_url.
This endpoint takes no parameters.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
provider | string | Yes | OAuth provider: github or google |
code_challenge | string | Yes | PKCE code_challenge (base64url SHA-256 of code_verifier, 43–128 chars) |
state_id | string | Yes | Per-attempt UUID v4 |
curl -X POST https://api.hoody.com/api/v1/auth/launch/initiate \ -H "Content-Type: application/json" \ -d '{ "provider": "github", "code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", "state_id": "550e8400-e29b-41d4-a716-446655440000" }'const result = await client.api.authentication.oauthLaunchInitiate({ provider: "github", code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", state_id: "550e8400-e29b-41d4-a716-446655440000"});{ "statusCode": 200, "data": { "launch_url": "https://api.hoody.com/api/v1/auth/launch/start?ticket=eyJhbGciOi..." }}GET /api/v1/auth/launch/start
Section titled “GET /api/v1/auth/launch/start”Consume the launch ticket and run the existing OAuth redirect flow. The popup navigates here. Sets Referrer-Policy: no-referrer.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
ticket | query | string | Yes | One-shot ticket from /launch/initiate response |
curl -X GET "https://api.hoody.com/api/v1/auth/launch/start?ticket=eyJhbGciOi..."const result = await client.api.authentication.oauthLaunchStart();This endpoint responds with an HTTP redirect (302) to the OAuth provider.
{ "statusCode": 410, "error": "Gone", "message": "Launch ticket expired or already consumed"}GET /api/v1/auth/device/authorize
Section titled “GET /api/v1/auth/device/authorize”The verification page navigates here. Consumes the device_verify_ticket and __Host-device_verify cookie atomically, then redirects to the provider with a server-injected device_binding. Sets Referrer-Policy: no-referrer.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
ticket | query | string | Yes | device_verify_ticket minted by /device/verify_code |
provider | query | string | Yes | OAuth provider: github or google |
curl -X GET "https://api.hoody.com/api/v1/auth/device/authorize?ticket=eyJhbGciOi...&provider=github"const result = await client.api.authentication.oauthDeviceAuthorize();This endpoint responds with an HTTP redirect (302) to the OAuth provider.
{ "statusCode": 403, "error": "Forbidden", "message": "Device verification cookie or ticket missing or invalid"}{ "statusCode": 410, "error": "Gone", "message": "Device verification ticket expired or already consumed"}POST /api/v1/auth/device/code
Section titled “POST /api/v1/auth/device/code”Start an RFC 8628 device authorization flow. Issues a device_code (polled by the TUI) and a short, hand-typeable user_code (shown to the human). Public, no authentication.
This endpoint takes no parameters.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
client_name | string | No | Shown on the verification page as “X is requesting access” |
code_challenge | string | No | Optional PKCE on the device flow itself; if present the poll requires the verifier |
curl -X POST https://api.hoody.com/api/v1/auth/device/code \ -H "Content-Type: application/json" \ -d '{ "client_name": "Hoody CLI" }'const result = await client.api.authentication.oauthDeviceCode({ client_name: "Hoody CLI"});{ "statusCode": 200, "data": { "device_code": "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234", "user_code": "ABCD-1234", "verification_uri": "https://hoody.com/device", "verification_uri_complete": "https://hoody.com/device?code=ABCD-1234", "interval": 5, "expires_in": 900 }}POST /api/v1/auth/device/token
Section titled “POST /api/v1/auth/device/token”Poll for device-flow tokens (RFC 8628 §3.5). Returns lifecycle errors (authorization_pending, slow_down, access_denied, expired_token) in 400 responses until approval, then 200 with the token set. Single-use on approval. Public, no authentication.
This endpoint takes no parameters.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
device_code | string | Yes | Device code from /device/code (64 hex chars) |
code_verifier | string | No | PKCE code_verifier. Required if code_challenge was provided to /device/code |
curl -X POST https://api.hoody.com/api/v1/auth/device/token \ -H "Content-Type: application/json" \ -d '{ "device_code": "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234" }'const result = await client.api.authentication.oauthDeviceToken({ device_code: "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234"});{ "statusCode": 200, "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expires_at": "2025-11-28T20:19:00.000Z", "expires_in": 86400, "refresh_expires_at": "2025-12-04T20:19:00.000Z", "refresh_expires_in": 604800, "user": { "id": "507f1f77bcf86cd799439011", "username": "john_doe", "alias": "John Doe", "email": "john.doe@example.com", "is_admin": false, "email_verified": true, "signup_method": "github", "avatar_url": "https://avatars.githubusercontent.com/u/1234567", "created_at": "2024-12-01T10:00:00.000Z", "updated_at": "2025-01-15T10:30:00.000Z" } }}{ "statusCode": 400, "data": { "error": "authorization_pending" }}POST /api/v1/auth/device/verify_code
Section titled “POST /api/v1/auth/device/verify_code”Verification page helper. On a live pending row, mints a one-time device_verify_ticket and sets the __Host-device_verify cookie. Leaks only client_name and a coarse status. Page-only.
This endpoint takes no parameters.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
user_code | string | Yes | XXXX-XXXX user code (dashes optional) |
curl -X POST https://api.hoody.com/api/v1/auth/device/verify_code \ -H "Content-Type: application/json" \ -d '{ "user_code": "ABCD-1234" }'const result = await client.api.authentication.oauthDeviceVerifyCode({ user_code: "ABCD-1234"});{ "statusCode": 200, "data": { "client_name": "Hoody CLI", "status": "pending" }}POST /api/v1/auth/intent/cancel
Section titled “POST /api/v1/auth/intent/cancel”Cancel a pending OAuth AuthIntent or 2FA temp_token. Idempotent. Used by the handoff page when the user dismisses the confirmation. Pass the intent or temp token in the Authorization header.
This endpoint takes no parameters.
curl -X POST https://api.hoody.com/api/v1/auth/intent/cancel \ -H "Authorization: Bearer <intent_or_temp_token>"const result = await client.api.authentication.oauthCancelIntent();This endpoint returns an empty 204 No Content response on success.
Two-factor authentication
Section titled “Two-factor authentication”GET /api/v1/users/auth/2fa/status
Section titled “GET /api/v1/users/auth/2fa/status”Check the current 2FA status for the authenticated user, including whether it is enabled and how many backup codes remain.
This endpoint takes no parameters.
curl -X GET https://api.hoody.com/api/v1/users/auth/2fa/status \ -H "Authorization: Bearer <token>"const result = await client.api.tfa.getStatus();{ "statusCode": 200, "message": "2FA status retrieved", "data": { "enabled": true, "verified": true, "enabled_at": "2025-01-14T21:00:00.000Z", "backup_codes_remaining": 8, "require_for_tokens": true }}{ "statusCode": 401, "error": "Unauthorized", "message": "Authentication token required", "data": { "attempts_remaining": 5 }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TOKEN | Authentication token missing | No authentication token was provided in the request | Include a valid JWT token in the Authorization header as Bearer <token> |
INVALID_TOKEN | Invalid authentication token | The provided authentication token is malformed or invalid | Obtain a new token by logging in again or using a valid auth token |
TOKEN_EXPIRED | Authentication token expired | The provided authentication token has expired | Obtain a new token by logging in again or refreshing your session |
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired | Generate a new code from your authenticator app and try again |
{ "statusCode": 429, "error": "Too Many Requests", "message": "Too many failed attempts. Account locked for 15 minutes.", "data": { "lockout_seconds": 900 }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
TWOFACTOR_RATE_LIMIT | 2FA verification locked | Too many failed 2FA verification attempts. Account is temporarily locked. | Wait for the lockout period to expire (15 minutes) before trying again |
POST /api/v1/users/auth/2fa/setup
Section titled “POST /api/v1/users/auth/2fa/setup”Begin 2FA setup. Requires the current password for verification. Returns a QR code for the authenticator app and backup codes.
This endpoint takes no parameters.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
password | string | Yes | Current account password for verification |
curl -X POST https://api.hoody.com/api/v1/users/auth/2fa/setup \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "password": "S3cure!Passw0rd" }'const result = await client.api.tfa.setup({ password: "S3cure!Passw0rd"});{ "statusCode": 200, "message": "2FA setup initiated", "data": { "qr_code": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...", "manual_entry_key": "JBSWY3DPEHPK3PXP", "backup_codes": [ "a1b2c3d4e5", "f6g7h8i9j0", "k1l2m3n4o5", "p6q7r8s9t0", "u1v2w3x4y5", "z6a7b8c9d0", "e1f2g3h4i5", "j6k7l8m9n0", "o1p2q3r4s5", "t6u7v8w9x0" ] }}{ "statusCode": 400, "error": "INCORRECT_PASSWORD", "message": "Incorrect password"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Invalid input parameters | One or more request parameters failed validation | Check the error message for specific field requirements and correct your input |
MISSING_REQUIRED_FIELD | Required field missing | One or more required fields are missing from the request | Include all required fields as specified in the API documentation |
INCORRECT_PASSWORD | Incorrect password | The provided password does not match the account password | Verify your password and try again |
TWOFACTOR_ALREADY_ENABLED | 2FA already enabled | Two-factor authentication is already enabled for this account | Disable 2FA first if you want to set it up again |
{ "statusCode": 401, "error": "Unauthorized", "message": "Authentication token required"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TOKEN | Authentication token missing | No authentication token was provided in the request | Include a valid JWT token in the Authorization header as Bearer <token> |
INVALID_TOKEN | Invalid authentication token | The provided authentication token is malformed or invalid | Obtain a new token by logging in again or using a valid auth token |
TOKEN_EXPIRED | Authentication token expired | The provided authentication token has expired | Obtain a new token by logging in again or refreshing your session |
POST /api/v1/users/auth/2fa/verify-setup
Section titled “POST /api/v1/users/auth/2fa/verify-setup”Complete 2FA setup by providing the first code from your authenticator app. This confirms the setup is working correctly.
This endpoint takes no parameters.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | 6-digit code from the authenticator app |
curl -X POST https://api.hoody.com/api/v1/users/auth/2fa/verify-setup \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "code": "123456" }'const result = await client.api.tfa.verifySetup({ code: "123456"});{ "statusCode": 200, "message": "2FA successfully enabled", "data": { "enabled": true, "enabled_at": "2025-01-14T21:00:00.000Z" }}{ "statusCode": 400, "error": "INVALID_OTP_CODE", "message": "Invalid or expired 2FA code"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Invalid input parameters | One or more request parameters failed validation | Check the error message for specific field requirements and correct your input |
MISSING_REQUIRED_FIELD | Required field missing | One or more required fields are missing from the request | Include all required fields as specified in the API documentation |
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired | Generate a new code from your authenticator app and try again |
TWOFACTOR_NOT_VERIFIED | 2FA setup not verified | 2FA setup was initiated but not yet verified | Complete the setup by verifying your first code |
{ "statusCode": 401, "error": "Unauthorized", "message": "Authentication token required", "data": { "attempts_remaining": 5 }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TOKEN | Authentication token missing | No authentication token was provided in the request | Include a valid JWT token in the Authorization header as Bearer <token> |
INVALID_TOKEN | Invalid authentication token | The provided authentication token is malformed or invalid | Obtain a new token by logging in again or using a valid auth token |
TOKEN_EXPIRED | Authentication token expired | The provided authentication token has expired | Obtain a new token by logging in again or refreshing your session |
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired | Generate a new code from your authenticator app and try again |
{ "statusCode": 429, "error": "Too Many Requests", "message": "Too many failed attempts. Account locked for 15 minutes.", "data": { "lockout_seconds": 900 }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
TWOFACTOR_RATE_LIMIT | 2FA verification locked | Too many failed 2FA verification attempts. Account is temporarily locked. | Wait for the lockout period to expire (15 minutes) before trying again |
POST /api/v1/users/auth/2fa/verify
Section titled “POST /api/v1/users/auth/2fa/verify”Complete login by verifying the 2FA code. Use the temp_token from the login response and provide either a 6-digit OTP code or a 10-character backup code.
This endpoint takes no parameters.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
temp_token | string | No | Temporary token from login response (valid for 5 minutes). Alternatively pass it as Authorization: Bearer |
code | string | Yes | 6-digit OTP code from authenticator app OR 10-character backup code |
response_mode | string | No | tokens (default) or intent |
curl -X POST https://api.hoody.com/api/v1/users/auth/2fa/verify \ -H "Content-Type: application/json" \ -d '{ "temp_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "code": "123456" }'const result = await client.api.tfa.verify({ temp_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", code: "123456"});{ "statusCode": 200, "message": "Authentication successful", "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "user": { "id": "507f1f77bcf86cd799439011", "alias": "John Doe" } }}{ "statusCode": 400, "error": "INVALID_OTP_CODE", "message": "Invalid or expired 2FA code"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Invalid input parameters | One or more request parameters failed validation | Check the error message for specific field requirements and correct your input |
MISSING_REQUIRED_FIELD | Required field missing | One or more required fields are missing from the request | Include all required fields as specified in the API documentation |
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired | Generate a new code from your authenticator app and try again |
INVALID_BACKUP_CODE | Invalid backup code | The provided backup code is incorrect or has already been used | Verify the backup code is correct and has not been used previously |
INVALID_TEMP_TOKEN | Invalid temporary token | The temporary token from login has expired or is invalid | Log in again to get a new temporary token |
{ "statusCode": 401, "error": "Unauthorized", "message": "Invalid or expired 2FA code", "data": { "attempts_remaining": 5 }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired | Generate a new code from your authenticator app and try again |
INVALID_BACKUP_CODE | Invalid backup code | The provided backup code is incorrect or has already been used | Verify the backup code is correct and has not been used previously |
{ "statusCode": 429, "error": "Too Many Requests", "message": "Too many failed attempts. Account locked for 15 minutes.", "data": { "lockout_seconds": 900 }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
TWOFACTOR_RATE_LIMIT | 2FA verification locked | Too many failed 2FA verification attempts. Account is temporarily locked. | Wait for the lockout period to expire (15 minutes) before trying again |
POST /api/v1/users/auth/2fa/backup-codes/regenerate
Section titled “POST /api/v1/users/auth/2fa/backup-codes/regenerate”Generate new backup codes. All existing backup codes are invalidated. Requires the current password and a current OTP code for security.
This endpoint takes no parameters.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
password | string | Yes | Current account password |
code | string | Yes | 6-digit OTP code from authenticator app |
curl -X POST https://api.hoody.com/api/v1/users/auth/2fa/backup-codes/regenerate \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "password": "S3cure!Passw0rd", "code": "123456" }'const result = await client.api.tfa.regenerateBackupCodes({ password: "S3cure!Passw0rd", code: "123456"});{ "statusCode": 200, "message": "Backup codes regenerated", "data": { "backup_codes": [ "a1b2c3d4e5", "f6g7h8i9j0", "k1l2m3n4o5", "p6q7r8s9t0", "u1v2w3x4y5", "z6a7b8c9d0", "e1f2g3h4i5", "j6k7l8m9n0", "o1p2q3r4s5", "t6u7v8w9x0" ] }}{ "statusCode": 400, "error": "INCORRECT_PASSWORD", "message": "Incorrect password"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Invalid input parameters | One or more request parameters failed validation | Check the error message for specific field requirements and correct your input |
MISSING_REQUIRED_FIELD | Required field missing | One or more required fields are missing from the request | Include all required fields as specified in the API documentation |
INCORRECT_PASSWORD | Incorrect password | The provided password does not match the account password | Verify your password and try again |
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired | Generate a new code from your authenticator app and try again |
TWOFACTOR_NOT_ENABLED | 2FA not enabled | Two-factor authentication is not enabled for this account | Set up 2FA first using the setup endpoint |
{ "statusCode": 401, "error": "Unauthorized", "message": "Authentication token required", "data": { "attempts_remaining": 5 }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TOKEN | Authentication token missing | No authentication token was provided in the request | Include a valid JWT token in the Authorization header as Bearer <token> |
INVALID_TOKEN | Invalid authentication token | The provided authentication token is malformed or invalid | Obtain a new token by logging in again or using a valid auth token |
TOKEN_EXPIRED | Authentication token expired | The provided authentication token has expired | Obtain a new token by logging in again or refreshing your session |
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired | Generate a new code from your authenticator app and try again |
{ "statusCode": 429, "error": "Too Many Requests", "message": "Too many failed attempts. Account locked for 15 minutes.", "data": { "lockout_seconds": 900 }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
TWOFACTOR_RATE_LIMIT | 2FA verification locked | Too many failed 2FA verification attempts. Account is temporarily locked. | Wait for the lockout period to expire (15 minutes) before trying again |
DELETE /api/v1/users/auth/2fa
Section titled “DELETE /api/v1/users/auth/2fa”Disable 2FA for the account. Requires the current password and a valid OTP code (or backup code) for security.
This endpoint takes no parameters.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
password | string | Yes | Current account password |
code | string | Yes | 6-digit OTP code from authenticator app OR backup code |
curl -X DELETE https://api.hoody.com/api/v1/users/auth/2fa \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "password": "S3cure!Passw0rd", "code": "123456" }'const result = await client.api.tfa.disable({ password: "S3cure!Passw0rd", code: "123456"});{ "statusCode": 200, "message": "2FA successfully disabled"}{ "statusCode": 400, "error": "INCORRECT_PASSWORD", "message": "Incorrect password"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Invalid input parameters | One or more request parameters failed validation | Check the error message for specific field requirements and correct your input |
MISSING_REQUIRED_FIELD | Required field missing | One or more required fields are missing from the request | Include all required fields as specified in the API documentation |
INCORRECT_PASSWORD | Incorrect password | The provided password does not match the account password | Verify your password and try again |
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired | Generate a new code from your authenticator app and try again |
INVALID_BACKUP_CODE | Invalid backup code | The provided backup code is incorrect or has already been used | Verify the backup code is correct and has not been used previously |
TWOFACTOR_NOT_ENABLED | 2FA not enabled | Two-factor authentication is not enabled for this account | Set up 2FA first using the setup endpoint |
{ "statusCode": 401, "error": "Unauthorized", "message": "Authentication token required", "data": { "attempts_remaining": 5 }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TOKEN | Authentication token missing | No authentication token was provided in the request | Include a valid JWT token in the Authorization header as Bearer <token> |
INVALID_TOKEN | Invalid authentication token | The provided authentication token is malformed or invalid | Obtain a new token by logging in again or using a valid auth token |
TOKEN_EXPIRED | Authentication token expired | The provided authentication token has expired | Obtain a new token by logging in again or refreshing your session |
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired | Generate a new code from your authenticator app and try again |
INVALID_BACKUP_CODE | Invalid backup code | The provided backup code is incorrect or has already been used | Verify the backup code is correct and has not been used previously |
{ "statusCode": 429, "error": "Too Many Requests", "message": "Too many failed attempts. Account locked for 15 minutes.", "data": { "lockout_seconds": 900 }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
TWOFACTOR_RATE_LIMIT | 2FA verification locked | Too many failed 2FA verification attempts. Account is temporarily locked. | Wait for the lockout period to expire (15 minutes) before trying again |
PUT /api/v1/users/auth/2fa/token-gate
Section titled “PUT /api/v1/users/auth/2fa/token-gate”Enable or disable the OTP requirement for token mutation operations. Setting enabled=false requires both the account password and a valid OTP code as a security downgrade.
This endpoint takes no parameters.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
enabled | boolean | Yes | true to require OTP for token mutations, false to skip the OTP gate |
password | string | Yes | Required when enabled=false (security downgrade requires primary-factor reauth) |
otp_code | string | No | TOTP code or backup code. Required when enabled=false |
curl -X PUT https://api.hoody.com/api/v1/users/auth/2fa/token-gate \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "enabled": true }'const result = await client.api.tfa.setTokenGate({ enabled: true});{ "statusCode": 200, "message": "Token gate preference updated", "data": { "require_for_tokens": true }}{ "statusCode": 400, "error": "OTP_REQUIRED", "message": "2FA verification required for this operation"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
OTP_REQUIRED | 2FA verification required | This operation requires 2FA verification because your account has 2FA enabled | Provide an otp_code field with a valid TOTP code or backup code |
TWOFACTOR_NOT_ENABLED | 2FA not enabled | Two-factor authentication is not enabled for this account | Set up 2FA first using the setup endpoint |
INCORRECT_PASSWORD | Incorrect password | The provided password does not match the account password | Verify your password and try again |
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired | Generate a new code from your authenticator app and try again |
INVALID_BACKUP_CODE | Invalid backup code | The provided backup code is incorrect or has already been used | Verify the backup code is correct and has not been used previously |
{ "statusCode": 401, "error": "INVALID_OTP_CODE", "message": "Invalid or expired 2FA code"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired | Generate a new code from your authenticator app and try again |
INCORRECT_PASSWORD | Incorrect password | The provided password does not match the account password | Verify your password and try again |
INVALID_BACKUP_CODE | Invalid backup code | The provided backup code is incorrect or has already been used | Verify the backup code is correct and has not been used previously |
{ "statusCode": 429, "error": "Too Many Requests", "message": "Too many failed attempts. Account locked for 15 minutes.", "data": { "lockout_seconds": 900 }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
TWOFACTOR_RATE_LIMIT | 2FA verification locked | Too many failed 2FA verification attempts. Account is temporarily locked. | Wait for the lockout period to expire (15 minutes) before trying again |