Skip to content
Hoody.com

The Hoody authentication API covers email/password sign-in, OAuth sign-in flows (PKCE for first-party web, GitHub and Google providers, an RFC-8628-inspired device flow for CLIs, and a popup-handoff launch flow), session and refresh-token management, and TOTP two-factor authentication. Use these endpoints to drive browser sign-in, integrate the CLI device flow, manage long-lived sessions, and gate sensitive token mutations behind a second factor. The control-plane surface (https://api.hoody.com) handles every flow below; container-scoped resources use the {projectId}-{containerId}-{service}-{instance}.{server}.containers.hoody.com hostname template.

Returns regions where free-tier servers exist, with boolean availability. Public, no auth required.

This endpoint takes no parameters.

{
"statusCode": 200,
"data": {
"regions": [
{ "region": "us-east", "country": "US", "city": "New York", "available": true },
{ "region": "eu-west", "country": "DE", "city": "Frankfurt", "available": true },
{ "region": "ap-southeast", "country": "SG", "city": "Singapore", "available": false }
]
}
}
Terminal window
curl https://api.hoody.com/api/v1/auth/available-regions

Returns the public sign-in configuration (such as which identity providers are enabled) used to drive the OAuth flow.

This endpoint takes no parameters.

{
"statusCode": 200,
"data": {
"appName": "Hoody",
"logoUrl": "https://app.hoody.com/logo.svg",
"primaryColor": "#5B6CFF",
"githubEnabled": true,
"googleEnabled": false,
"signupEnabled": true,
"inviteRequired": false
}
}
Terminal window
curl https://api.hoody.com/api/v1/auth/config

Redeems the auth_intent_token from an intent-mode login (sent as Authorization: Bearer {auth_intent_token}, not a session token) for a single-use authorization code bound to the PKCE code_challenge and redirect_uri. Exchange the code at POST /api/v1/auth/exchange. SDK callers: the generated method sends the client’s configured token and has no per-call Authorization option, so call it on a client whose token is the auth_intent_token; a session token here is refused with 401.

FieldTypeRequiredDescription
code_challengestringYesPKCE code_challenge (base64url SHA-256 of code_verifier), exactly 43 characters
redirect_uristringYesHTTPS URL to redirect to after OAuth completes (must be on an allowed domain)
Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/authorize \
-H 'Content-Type: application/json' \
-d '{
"code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"redirect_uri": "https://app.example.com/auth/callback"
}'
{
"statusCode": 200,
"data": {
"code": "9f2c4e1a7b3d5f6081a2c3e4f5061728394a5b6c7d8e9f00112233445566778"
}
}

code is a single-use authorization code (64 hex characters, valid 60 seconds). Redeem it at POST /api/v1/auth/exchange with the code_verifier and the same redirect_uri.

Completes the PKCE authorization-code flow by exchanging an authorization code and its code verifier for authentication tokens.

FieldTypeRequiredDescription
codestringYesAuthorization code returned on the redirect (64 hex characters)
code_verifierstringYesPKCE code verifier used to compute the original code_challenge
redirect_uristringYesHTTPS URL that must match the value sent to /auth/authorize
Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/exchange \
-H 'Content-Type: application/json' \
-d '{
"code": "4f3e2a1b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f",
"code_verifier": "qPsKJFJb5K9XqLkVz4a3bYpQvWjMnRtHuGxZeDcVbAo",
"redirect_uri": "https://app.example.com/auth/callback"
}'
{
"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,
"identity_claim": {
"kid": "v1",
"payload_b64": "eyJjbGFpbV90eXBlIjoiaWRlbnRpdHkiLCJpc3MiOiJob29keS1hcGkiLCJzdWIiOiI1MDdmMWY3N2JjZjg2Y2Q3OTk0MzkwMTEiLCJ1c2VybmFtZSI6ImpvaG5fZG9lIiwidHlwZSI6InVzZXIiLCJpYXQiOjE3NDEyOTAwMDAsImV4cCI6MTc0Mzg4MjAwMCwia2lkIjoidjEifQ",
"signature_hex": "abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef12"
},
"user": {
"id": "507f1f77bcf86cd799439011",
"username": "john_doe",
"alias": "John Doe",
"email": "john.doe@example.com",
"is_banned": false,
"email_verified": true,
"avatar_url": null,
"signup_method": "email",
"created_at": "2024-12-01T10:00:00.000Z",
"updated_at": "2025-01-15T10:30:00.000Z"
},
"server": {
"id": "507f1f77bcf86cd799439099",
"name": "node-example-1",
"country": "US",
"region": "us-east",
"city": "New York",
"datacenter": "nyc1",
"is_ready": true
},
"project": { "id": "507f1f77bcf86cd799439020", "alias": "default" },
"container": { "id": "507f1f77bcf86cd799439030", "name": "alpha" }
}
}

Redirects the browser to GitHub for OAuth authentication. Browser-only endpoint.

NameInTypeRequiredDescription
clientquerystringNoOptional client-declared source channel for analytics: web, ssh, webssh, cli, sdk, or agent. Unrecognised values are recorded as unknown.
intentquerystringNoOAuth intent: login (default). star_check is still accepted and behaves like login.
redirect_uriquerystringYesFrontend URL to redirect to after OAuth completes (must be on an allowed domain).
code_challengequerystringYesPKCE code_challenge (base64url SHA-256 of code_verifier). Required, all OAuth flows must use PKCE post-migration.
invite_codequerystringNoOptional invite code (coupon) captured from the signup link. Normalized and hashed at redirect time; only the hash travels in the OAuth state, never the raw code. Memorized hash-only on a NEW account and applied automatically; not validated here.
HTTP/1.1 302 Found
Location: https://github.com/login/oauth/authorize?...
Terminal window
curl -i 'https://api.hoody.com/api/v1/auth/github?redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Fcallback&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM'

Handles the GitHub OAuth callback. Browser-only endpoint.

NameInTypeRequiredDescription
codequerystringNoAuthorization code returned by GitHub.
statequerystringYesSigned state JWT issued by /auth/github.
errorquerystringNoProvider-side failure code (e.g. access_denied). Present instead of code when the user declines.
error_descriptionquerystringNoProvider-supplied human-readable failure description.
error_uriquerystringNoProvider-supplied failure reference URL.
HTTP/1.1 302 Found
Location: https://app.example.com/auth/callback?...
Terminal window
curl -i 'https://api.hoody.com/api/v1/auth/github/callback?code=acbd1234&state=<state-jwt>'

Redirects the browser to Google for OAuth authentication. Browser-only endpoint.

NameInTypeRequiredDescription
clientquerystringNoOptional client-declared source channel for analytics: web, ssh, webssh, cli, sdk, or agent. Unrecognised values are recorded as unknown.
redirect_uriquerystringYesFrontend URL to redirect to after OAuth completes (must be on an allowed domain).
code_challengequerystringYesPKCE code_challenge (base64url SHA-256 of code_verifier). Required, all OAuth flows must use PKCE post-migration.
invite_codequerystringNoOptional invite code (coupon) captured from the signup link. Normalized and hashed at redirect time; only the hash travels in the OAuth state, never the raw code. Memorized hash-only on a NEW account and applied automatically; not validated here.
HTTP/1.1 302 Found
Location: https://accounts.google.com/o/oauth2/v2/auth?...
Terminal window
curl -i 'https://api.hoody.com/api/v1/auth/google?redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Fcallback&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM'

Handles the Google OAuth callback. Browser-only endpoint.

NameInTypeRequiredDescription
codequerystringNoAuthorization code returned by Google.
statequerystringYesSigned state JWT issued by /auth/google.
errorquerystringNoProvider-side failure code (e.g. access_denied). Present instead of code when the user declines.
error_descriptionquerystringNoProvider-supplied human-readable failure description.
error_uriquerystringNoProvider-supplied failure reference URL.
HTTP/1.1 302 Found
Location: https://app.example.com/auth/callback?...
Terminal window
curl -i 'https://api.hoody.com/api/v1/auth/google/callback?code=acbd1234&state=<state-jwt>'

GET endpoint the verification page navigates to. Verifies the provider is fully configured (302 back to the device page with ?error=provider_unavailable, ticket intact, when not), then consumes the device_verify_ticket + __Host-device_verify cookie atomically and redirects to the provider with a server-injected device_binding + attempt nonce. Sets Referrer-Policy: no-referrer.

NameInTypeRequiredDescription
ticketquerystringYesdevice_verify_ticket from /device/verify_code.
providerquerystringYesOAuth provider. One of github or google.
HTTP/1.1 302 Found
Location: https://github.com/login/oauth/authorize?...
Terminal window
curl -i 'https://api.hoody.com/api/v1/auth/device/authorize?ticket=<device_verify_ticket>&provider=github'

Issues a device_code (polled by the CLI) and a short hand-typeable user_code (shown to the human). Public, no auth. RFC-8628-inspired but deliberately not a standards-compliant device grant: no client_id/grant_type, lifecycle errors are nested under data, and the poll interval is a fixed 5 s after slow_down.

FieldTypeRequiredDescription
client_namestringNoShown on the verification page as “X is requesting access”.
clientstringNoOptional client-declared source channel for analytics: web, ssh, webssh, cli, sdk, or agent. Unrecognised values are recorded as unknown; never affects authentication.
code_challengestringNoOptional PKCE on the device flow itself; if present the poll REQUIRES the verifier.
Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/device/code \
-H 'Content-Type: application/json' \
-d '{
"client_name": "Hoody CLI",
"client": "cli",
"code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
}'
{
"statusCode": 200,
"data": {
"device_code": "7c2b4e8a9d1f3c5b7e9a2c4d6f8b1a3c5e7d9f2b4a6c8e1d3f5b7a9c2e4d6f8b1",
"user_code": "ABCD-1234",
"verification_uri": "https://api.hoody.com/auth/device",
"verification_uri_complete": "https://api.hoody.com/auth/device?code=ABCD-1234",
"interval": 5,
"expires_in": 600
}
}

Page-only 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 coarse status.

FieldTypeRequiredDescription
user_codestringYesXXXX-XXXX user code (dashes optional).
Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/device/verify_code \
-H 'Content-Type: application/json' \
-d '{
"user_code": "ABCD-1234"
}'
{
"statusCode": 200,
"data": {
"status": "pending",
"client_name": "Hoody CLI",
"expires_in": 300,
"device_verify_ticket": "5b8e2a1c4d7f9a3b6c8e1d2f4a6b8c0e2d4f6a8b1c3e5d7f9a1b3c5e7d9f1a3b"
}
}

status is "pending" while the code is still live. When the code has already been approved, status is "approved" and the other fields are absent.

Password sign-in for the device authorize step (cookie + ticket gated). Page-only helper. Verifies email/username + password with FULL login parity (shared per-account throttle, timing-normalized bcrypt) behind the device_verify_ticket + __Host-device_verify cookie gate. NEVER returns session tokens: no-2FA → {status:"approved"} (tokens mint only at /device/token); 2FA → {requires_2fa,temp_token} (device-bound partial, no code_challenge). Credential failures do NOT consume the ticket. Feature-flag off → 404; schema-invalid body → 422.

FieldTypeRequiredDescription
ticketstringYesdevice_verify_ticket from /device/verify_code.
usernamestringNoUsername (alternative to email).
emailstringNoEmail address (alternative to username).
passwordstringYesAccount password.
Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/device/login \
-H 'Content-Type: application/json' \
-b '__Host-device_verify=<device_verify_cookie>' \
-d '{
"ticket": "5b8e2a1c4d7f9a3b6c8e1d2f4a6b8c0e2d4f6a8b1c3e5d7f9a1b3c5e7d9f1a3b",
"username": "john_doe",
"password": "SecurePassword123!"
}'
{
"statusCode": 200,
"data": {
"status": "approved"
}
}

Page-only helper of the RFC-8628-inspired device flow. Cookie + ticket gated, no credentials required, possession of the live ticket + cookie is the refusing authority. Flips the pending row to denied; the terminal poll then reports access_denied. Once a row has been approved it can never be un-approved.

FieldTypeRequiredDescription
ticketstringYesdevice_verify_ticket from /device/verify_code.
Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/device/deny \
-H 'Content-Type: application/json' \
-b '__Host-device_verify=<device_verify_cookie>' \
-d '{
"ticket": "5b8e2a1c4d7f9a3b6c8e1d2f4a6b8c0e2d4f6a8b1c3e5d7f9a1b3c5e7d9f1a3b"
}'
{
"statusCode": 200,
"data": { "status": "denied" }
}

Polled by the CLI while the user completes the browser step. 400 + {data:{error}} for lifecycle states (authorization_pending|slow_down|access_denied|expired_token), 200 + token-set on approval (single-use; also requires the approving user’s session generation to still be current: a password reset / logout-all after approval yields expired_token), 429 on the outer flood guard. Public, no auth. Lifecycle errors are nested under data, unlike RFC 8628 §3.5.

FieldTypeRequiredDescription
device_codestringYesThe device_code returned by /device/code.
code_verifierstringNoPKCE code_verifier. Required if code_challenge was set when the code was issued.
Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/device/token \
-H 'Content-Type: application/json' \
-d '{
"device_code": "7c2b4e8a9d1f3c5b7e9a2c4d6f8b1a3c5e7d9f2b4a6c8e1d3f5b7a9c2e4d6f8b1",
"code_verifier": "qPsKJFJb5K9XqLkVz4a3bYpQvWjMnRtHuGxZeDcVbAo"
}'
{
"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,
"identity_claim": {
"kid": "v1",
"payload_b64": "eyJjbGFpbV90eXBlIjoiaWRlbnRpdHkiLCJpc3MiOiJob29keS1hcGkiLCJzdWIiOiI1MDdmMWY3N2JjZjg2Y2Q3OTk0MzkwMTEiLCJ1c2VybmFtZSI6ImpvaG5fZG9lIiwidHlwZSI6InVzZXIiLCJpYXQiOjE3NDEyOTAwMDAsImV4cCI6MTc0Mzg4MjAwMCwia2lkIjoidjEifQ",
"signature_hex": "abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef12"
},
"user": {
"id": "507f1f77bcf86cd799439011",
"username": "john_doe",
"alias": "John Doe",
"email": "john.doe@example.com",
"email_verified": true,
"signup_method": "email",
"avatar_url": null,
"created_at": "2024-12-01T10:00:00.000Z",
"updated_at": "2025-01-15T10:30:00.000Z"
},
"server": {
"id": "507f1f77bcf86cd799439099",
"name": "node-example-1",
"country": "US",
"region": "us-east",
"city": "New York",
"datacenter": "nyc1",
"is_ready": true
},
"project": { "id": "507f1f77bcf86cd799439020", "alias": "default" },
"container": { "id": "507f1f77bcf86cd799439030", "name": "alpha", "status": "running" }
}
}

POST endpoint that issues a one-shot launch ticket bound to the request Origin header. The frontend navigates the popup to the returned launch_url, which consumes the ticket and runs the existing PKCE-protected OAuth flow with state_id + opener_origin plumbed through.

FieldTypeRequiredDescription
providerstringYesOAuth provider. One of github or google.
clientstringNoOptional client-declared source channel for analytics.
code_challengestringYesPKCE code_challenge (base64url SHA-256 of code_verifier, exactly 43 chars).
state_idstringYesPer-attempt UUID v4, plumbed through state JWT, cookie name, fragment, message filter.
Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/launch/initiate \
-H 'Content-Type: application/json' \
-H 'Origin: https://app.example.com' \
-d '{
"provider": "github",
"code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"state_id": "0d3b7c20-4e2d-4b9a-8f01-2a3b4c5d6e7f"
}'
{
"statusCode": 200,
"data": {
"launch_url": "https://api.hoody.com/api/v1/auth/launch/start?ticket=8a4f2b6c1d3e5f7a9b1c3d5e7f9a1b3c"
}
}

GET endpoint the popup navigates to. Consumes the launch ticket atomically and runs the existing OAuth redirect flow. Sets Referrer-Policy: no-referrer.

NameInTypeRequiredDescription
ticketquerystringYesOne-shot ticket from /launch/initiate response.
HTTP/1.1 302 Found
Location: https://github.com/login/oauth/authorize?...
Terminal window
curl -i 'https://api.hoody.com/api/v1/auth/launch/start?ticket=<launch_ticket>'

POST endpoint with Authorization: Bearer <intent or temp_token>. Idempotent. Used by the handoff page when the user dismisses the confirmation.

This endpoint takes no parameters and no request body.

The 204 response has no body: the cancellation is confirmed by the status code alone.

Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/intent/cancel \
-H 'Authorization: Bearer <intent_or_temp_token>'

Create a new account with email and password. A verification email will be sent. The account is not active until the email is verified.

FieldTypeRequiredDescription
emailstringYesEmail address for the new account.
passwordstringYesPassword (min 12 chars, at most 72 UTF-8 bytes, at least 3 of the 4 character classes: uppercase, lowercase, number, special).
regionstringNoOptional preferred server region (e.g. eu-west). If omitted, auto-assigned by GeoIP proximity.
invite_codestringNoOptional invite code (coupon) captured from the signup link. Memorized (hash-only) and applied automatically after email verification; not validated at signup.
clientstringNoOptional client-declared source channel for analytics.
Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/signup \
-H 'Content-Type: application/json' \
-d '{
"email": "john.doe@example.com",
"password": "SecurePassword123!",
"region": "eu-west",
"invite_code": "WELCOME2026"
}'
{
"statusCode": 200,
"message": "Account created. Check your email to verify.",
"data": { "email": "john.doe@example.com" }
}

Verify the email address using the token from the verification email. Default response returns full login credentials. When response_mode=intent + code_challenge are provided, returns an opaque auth_intent_token for PKCE exchange (hosted auth UI flow). If 2FA is enabled on the account, returns requires_2fa + temp_token instead. See Identity claims for the format of the returned identity_claim.

FieldTypeRequiredDescription
clientstringNoOptional client-declared source channel for analytics.
tokenstringYesVerification token from the email link (64 hex chars).
response_modestringNoResponse shape. tokens (default) returns access/refresh tokens. intent returns an opaque auth_intent_token for PKCE exchange.
code_challengestringNoPKCE code_challenge. Required when response_mode=intent.
Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/verify-email \
-H 'Content-Type: application/json' \
-d '{
"token": "9a4f2b6c1d3e5f7a9b1c3d5e7f9a1b3c5e7d9f2b4a6c8e1d3f5b7a9c2e4d6f8b1"
}'
{
"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,
"identity_claim": {
"kid": "v1",
"payload_b64": "eyJjbGFpbV90eXBlIjoiaWRlbnRpdHkiLCJpc3MiOiJob29keS1hcGkiLCJzdWIiOiI1MDdmMWY3N2JjZjg2Y2Q3OTk0MzkwMTEiLCJ1c2VybmFtZSI6ImpvaG5fZG9lIiwidHlwZSI6InVzZXIiLCJpYXQiOjE3NDEyOTAwMDAsImV4cCI6MTc0Mzg4MjAwMCwia2lkIjoidjEifQ",
"signature_hex": "abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef12"
},
"user": {
"id": "507f1f77bcf86cd799439011",
"username": "john_doe",
"alias": "John Doe",
"email": "john.doe@example.com",
"email_verified": true,
"signup_method": "email",
"avatar_url": null,
"created_at": "2024-12-01T10:00:00.000Z",
"updated_at": "2025-01-15T10:30:00.000Z"
},
"server": {
"id": "507f1f77bcf86cd799439099",
"name": "node-example-1",
"country": "US",
"region": "us-east",
"city": "New York",
"datacenter": "nyc1",
"is_ready": true
},
"project": { "id": "507f1f77bcf86cd799439020", "alias": "default" },
"container": { "id": "507f1f77bcf86cd799439030", "name": "alpha" }
}
}

Send a password reset email. Always returns success to prevent email enumeration.

FieldTypeRequiredDescription
emailstringYesEmail address associated with the account.
Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/forgot-password \
-H 'Content-Type: application/json' \
-d '{
"email": "john.doe@example.com"
}'
{
"statusCode": 200,
"message": "If an account exists for that email, a password reset link has been sent."
}

Set a new password using the reset token from the password reset email.

FieldTypeRequiredDescription
tokenstringYesPassword reset token from the email link (64 hex chars).
passwordstringYesNew password (min 12 chars, at most 72 UTF-8 bytes).
Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/reset-password \
-H 'Content-Type: application/json' \
-d '{
"token": "9a4f2b6c1d3e5f7a9b1c3d5e7f9a1b3c5e7d9f2b4a6c8e1d3f5b7a9c2e4d6f8b1",
"password": "NewSecurePassword123!"
}'
{
"statusCode": 200,
"message": "Password has been reset. You can now log in."
}

Resend the email verification link. Always returns success to prevent email enumeration.

FieldTypeRequiredDescription
emailstringYesEmail address to resend verification to.
Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/resend-verification \
-H 'Content-Type: application/json' \
-d '{
"email": "john.doe@example.com"
}'
{
"statusCode": 200,
"message": "If the email is registered and unverified, a new verification link has been sent."
}

Authenticate with username and password to receive a JWT access token (expires in 1 day) and refresh token (expires in 7 days). Use the access token in the Authorization header for subsequent requests: Authorization: Bearer {token}. If 2FA is enabled, the response carries requires_2fa + temp_token instead. Complete the flow via Verify 2FA code during login. On success, the response includes an identity claim under data.identity_claim proving “Hoody authenticated this user”.

FieldTypeRequiredDescription
usernamestringNoUsername (alphanumeric, underscores, hyphens; alternative to email).
emailstringNoEmail address (alternative to username).
passwordstringYesAccount password, checked against the stored credential. Login applies no strength rule of its own, so a password created under an earlier policy still works.
response_modestringNoResponse shape. tokens (default) returns access/refresh tokens. intent returns an opaque auth_intent_token for PKCE exchange (hosted auth UI only).
clientstringNoOptional client-declared source channel for analytics.
code_challengestringNoPKCE code_challenge. Required when response_mode=intent.
Terminal window
curl -X POST https://api.hoody.com/api/v1/users/auth/login \
-H 'Content-Type: application/json' \
-d '{
"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,
"identity_claim": {
"kid": "v1",
"payload_b64": "eyJjbGFpbV90eXBlIjoiaWRlbnRpdHkiLCJpc3MiOiJob29keS1hcGkiLCJzdWIiOiI1MDdmMWY3N2JjZjg2Y2Q3OTk0MzkwMTEiLCJ1c2VybmFtZSI6ImpvaG5fZG9lIiwidHlwZSI6InVzZXIiLCJpYXQiOjE3NDEyOTAwMDAsImV4cCI6MTc0Mzg4MjAwMCwia2lkIjoidjEifQ",
"signature_hex": "abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef12"
},
"client_ip": "192.168.1.100",
"recent_login_ips": [
{ "ip": "192.168.1.100", "timestamp": "2025-11-27T10:30:00.000Z" },
{ "ip": "10.0.0.5", "timestamp": "2025-11-26T09:15:00.000Z" }
],
"auth_token_count": 2,
"user": {
"id": "507f1f77bcf86cd799439011",
"username": "john_doe",
"alias": "John Doe",
"email": "john.doe@example.com",
"is_banned": false,
"email_verified": true,
"metadata": {},
"created_at": "2024-12-01T10:00:00.000Z",
"updated_at": "2025-11-27T10:30:00.000Z"
},
"server": {
"id": "507f1f77bcf86cd799439099",
"name": "node-example-1",
"country": "US",
"region": "us-east",
"city": "New York",
"datacenter": "nyc1",
"is_ready": true
},
"project": { "id": "507f1f77bcf86cd799439020", "alias": "default" },
"container": { "id": "507f1f77bcf86cd799439030", "name": "alpha" }
}
}

Signs the account out on EVERY device, not only this client. Called with a session JWT, it revokes every access and refresh JWT issued to the account before this moment: other browsers, the CLI, other apps and this client all have to sign in again. Long-lived auth tokens (hdy_…) are not affected; revoke those with DELETE /api/v1/auth/tokens/{id}. Called with an auth token or HTTP Basic credentials, nothing is revoked and only the audit entry is written. A client should also drop its own stored access token, refresh token and credentials, or its automatic re-login signs it straight back in. Creates an audit log entry. Works even for banned users.

This endpoint takes no parameters and no request body.

{
"statusCode": 200,
"message": "Logout successful"
}
Terminal window
curl -X POST https://api.hoody.com/api/v1/users/auth/logout \
-H 'Authorization: Bearer <jwt_token>'

Exchange a valid refresh token for a new access token and new refresh token. Send the refresh token in the Authorization header: Authorization: Bearer {refreshToken}.

FieldTypeRequiredDescription
refreshTokenstringYesValid refresh token from previous login/refresh.
Terminal window
curl -X POST https://api.hoody.com/api/v1/users/auth/refresh \
-H 'Content-Type: application/json' \
-d '{
"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
}
}

Retrieve the profile of the currently authenticated user. Works with JWT, auth token, or Basic authentication. When authenticated with an auth token, response includes data.auth_token introspection details (permissions and realm restrictions). This endpoint works even for banned users (read-only access). Note: when authenticated with an auth token that lacks the resources.read_account permission, the response is reduced to identity fields (id, username, alias, public_key, timestamps); email and other account PII are omitted.

This endpoint takes no parameters.

{
"statusCode": 200,
"message": "Current user retrieved successfully",
"data": {
"id": "507f1f77bcf86cd799439011",
"username": "john_doe",
"alias": "John Doe",
"email": "john.doe@example.com",
"public_key": "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234",
"metadata": { "department": "Engineering" },
"is_banned": false,
"email_verified": true,
"avatar_url": "https://avatars.githubusercontent.com/u/1234567",
"signup_method": "email",
"free_tier_unlocked": true,
"free_tier_unlocked_at": "2026-06-24T10:00:00.000Z",
"free_tier_unlock_source": "invite_code",
"onboarding": { "hub_tour_v1": "2026-06-30T10:00:00.000Z" },
"created_at": "2024-12-01T10:00:00.000Z",
"updated_at": "2025-11-27T10:30:00.000Z",
"pending_pool_invitations": 0
}
}
Terminal window
curl https://api.hoody.com/api/v1/users/auth/me \
-H 'Authorization: Bearer <jwt_token>'

Retrieve the profile of the currently authenticated user. Alias of GET /api/v1/users/auth/me returning the same shape and behaviour. Works with JWT, auth token, or Basic authentication. When authenticated with an auth token, response includes data.auth_token introspection details (permissions and realm restrictions). This endpoint works even for banned users (read-only access). Note: when authenticated with an auth token that lacks the resources.read_account permission, the response is reduced to identity fields (id, username, alias, public_key, timestamps); email and other account PII are omitted.

This endpoint takes no parameters.

{
"statusCode": 200,
"message": "Current user retrieved successfully",
"data": {
"id": "507f1f77bcf86cd799439011",
"username": "john_doe",
"alias": "John Doe",
"email": "john.doe@example.com",
"public_key": "a1b2c3d4e5f6789012345678901234567890abcdefabcdefabcdefabcdef1234",
"metadata": {},
"is_banned": false,
"email_verified": true,
"avatar_url": null,
"signup_method": "email",
"free_tier_unlocked": true,
"free_tier_unlocked_at": "2026-06-24T10:00:00.000Z",
"free_tier_unlock_source": "invite_code",
"onboarding": { "hub_tour_v1": "2026-06-30T10:00:00.000Z" },
"created_at": "2024-12-01T10:00:00.000Z",
"updated_at": "2025-11-27T10:30:00.000Z",
"pending_pool_invitations": 0
}
}
Terminal window
curl https://api.hoody.com/api/v1/users/me \
-H 'Authorization: Bearer <jwt_token>'

Returns the ED25519 public key(s) used by Hoody to sign all API responses (X-Hoody-Signature header), identity claims issued at login, and container authorization claims. No authentication required; this endpoint is intentionally public.

Third-party verification flow:

  1. Fetch this endpoint once (cache for 24 hours or more).
  2. Locate the key by kid from the keys[] array. The keys[] array is a rotation window: a next key may be pre-published before it is used, and the previous key is retained until all in-flight claims have expired. Always look up by kid, not by array index.
  3. For response signatures: parse X-Hoody-Signature: t=<unix_ts>,kid=<keyId>,m=<method>,s=<status>,path=<request_url>,sig=<128-hex> and verify sig against the newline-joined tuple ${t}\n${method}\n${status}\n${path}\n${responseBodyUtf8String}.
  4. For identity and container claims: verify claim.signature_hex against the UTF-8 bytes of claim.payload_b64.
  5. If kid in a signature/claim does not match any cached key, re-fetch this endpoint.

This endpoint takes no parameters.

{
"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>,m=<method>,s=<status>,path=<request_url>,sig=<hex>",
"response_signed_data": "<t>\n<method>\n<status>\n<path>\n<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
}
}
}

The response is itself signed with X-Hoody-Signature; the same algorithm applies (see Third-party verification flow above).

Terminal window
curl https://api.hoody.com/api/v1/meta/public-key

An identity claim is an ED25519-signed credential that proves “Hoody authenticated this user” to systems outside Hoody: your own apps, partner services, anything that can fetch a public key. Third parties verify a claim offline against the ED25519 public key returned by GET /api/v1/meta/public-key; no network call back to Hoody is required.

Identity claims are returned by the following flows:

  • POST /api/v1/users/auth/login (login, when response_mode=tokens or unset and signing is configured)
  • POST /api/v1/users/auth/2fa/verify (2FA verify, same conditions)
  • POST /api/v1/auth/verify-email (verify-email, tokens mode)
  • POST /api/v1/auth/device/token (device-flow terminal, on approval)
  • POST /api/v1/users/auth/identity-claim (audience-bound re-issue)

They are omitted when response_mode=intent is selected and when identity signing is unavailable. Container claims use the same wire format with claim_type set to "container". See the containers reference for how to verify a payload.aud against a specific {projectId}-{containerId}-{service}-{instance}.{server}.containers.hoody.com hostname.

FieldTypeDescription
kidstringKey identifier. Look up the matching public_key_b64url in GET /api/v1/meta/public-key keys[].
payload_b64stringbase64url-encoded JSON payload. The signature is computed over the UTF-8 bytes of this exact string.
signature_hexstringED25519 detached signature (128 hex characters = 64 bytes).
FieldTypeDescription
claim_typestringAlways "identity" for identity claims (vs "container" for container claims).
issstringAlways "hoody-api".
substringUser ID (24 hex characters).
usernamestringUsername of the authenticated user.
typestringAccount type: "user" or "admin". Informational only.
iatnumberIssued-at (Unix seconds).
expnumberExpiry (Unix seconds). Default ~30 days for login-derived claims; shorter for re-issued audience-bound claims.
kidstringKey identifier, must match bundle.kid.
audstring(Audience-bound re-issued claims only) Consumer identifier the claim is bound to.

When verifying a claim, run every check below. A failure on any one is a rejection.

  1. Locate the key: look up bundle.kid in the cached keys[] from GET /api/v1/meta/public-key AND verify payload.kid === bundle.kid. Re-fetch the endpoint if the kid is unknown.
  2. Verify the ED25519 detached signature over the UTF-8 bytes of bundle.payload_b64.
  3. payload.claim_type === "identity".
  4. payload.iss === "hoody-api".
  5. payload.exp > now and payload.exp > payload.iat.
  6. payload.iat <= now + 300 (5 minute clock-skew tolerance).
  7. Audience binding (strict two-way): if your verifier expects an audience, payload.aud MUST equal exactly the verifier’s identifier. A claim with no aud field is rejected by any audience-aware verifier; a claim with an aud that does not match is rejected.
import * as crypto from 'crypto';
// ED25519 SPKI prefix (ASN.1 DER header for the algorithm OID + the raw 32-byte key).
// Required by Node's crypto.verify when the key is supplied as DER/SPKI.
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
interface PublicKeySet {
keys: Array<{ kid: string; public_key_b64url: string }>;
}
interface IdentityClaim {
kid: string;
payload_b64: string;
signature_hex: string;
}
async function loadKeys(): Promise<PublicKeySet> {
const res = await fetch('https://api.hoody.com/api/v1/meta/public-key');
const json = await res.json() as { data: PublicKeySet };
return json.data;
}
export function verifyIdentityClaim(
claim: IdentityClaim,
keys: PublicKeySet,
expectedAudience?: string
): { ok: true; payload: any } | { ok: false; reason: string } {
// (1) Locate the key by kid.
const entry = keys.keys.find((k) => k.kid === claim.kid);
if (!entry) return { ok: false, reason: 'unknown kid' };
// Decode the raw 32-byte public key, then prefix with the ED25519 SPKI OID.
const rawPubKey = Buffer.from(entry.public_key_b64url, 'base64url');
const derPubKey = Buffer.concat([ED25519_SPKI_PREFIX, rawPubKey]);
// Verify the detached signature over the UTF-8 bytes of payload_b64.
const signatureOk = crypto.verify(
null,
Buffer.from(claim.payload_b64, 'utf8'),
{ key: derPubKey, format: 'der', type: 'spki' },
Buffer.from(claim.signature_hex, 'hex')
);
if (!signatureOk) return { ok: false, reason: 'signature mismatch' };
// Decode the payload for the structural checks.
const payload = JSON.parse(Buffer.from(claim.payload_b64, 'base64url').toString('utf8'));
const now = Math.floor(Date.now() / 1000);
const checks: Array<[string, boolean]> = [
['claim_type === "identity"', payload.claim_type === 'identity'],
['iss === "hoody-api"', payload.iss === 'hoody-api'],
['exp > now', payload.exp > now],
['exp > iat', payload.exp > payload.iat],
['iat <= now + 300', payload.iat <= now + 300],
['payload.kid === bundle.kid', payload.kid === claim.kid],
[
'audience binding',
expectedAudience === undefined
? true
: typeof payload.aud === 'string' && payload.aud === expectedAudience
]
];
for (const [label, ok] of checks) {
if (!ok) return { ok: false, reason: `check failed: ${label}` };
}
return { ok: true, payload };
}
kid: v1
payload_b64:
eyJjbGFpbV90eXBlIjoiaWRlbnRpdHkiLCJpc3MiOiJob29keS1hcGkiLCJzdWIiOiI1MDdmMWY3N2JjZjg2Y2Q3OTk0MzkwMTEiLCJ1c2VybmFtZSI6ImpvaG5fZG9lIiwidHlwZSI6InVzZXIiLCJpYXQiOjE3NDEyOTAwMDAsImV4cCI6MTc0Mzg4MjAwMCwia2lkIjoidjEifQ
payload (decoded):
{
"claim_type": "identity",
"iss": "hoody-api",
"sub": "507f1f77bcf86cd799439011",
"username": "john_doe",
"type": "user",
"iat": 1741290000,
"exp": 1743882000,
"kid": "v1"
}
signature_hex:
abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234
abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234

This example uses aud-less payload semantics (login-derived claim). See Security below for why re-issued claims with aud are preferred.

Gating proxied apps with an identity claim

Section titled “Gating proxied apps with an identity claim”

When a Hoody container fronts another application (via the proxy), the application can require callers to present a valid identity claim. Three patterns are supported, in order of preference:

The cleanest path: grant a proxy permission with the built-in hoody-identity group. The edge validates the X-Hoody-Identity-Claim (and related) headers before forwarding the request, no app-side crypto required. See the proxy permissions reference for the full grant syntax.

2. App-level verification via a non-reserved header

Section titled “2. App-level verification via a non-reserved header”

If the app prefers to do its own verification, carry the claim in a header outside the reserved X-Hoody-Identity-* namespace. The recommended convention is:

X-Hoody-Claim: <payload_b64>.<signature_hex>

The app pins a known kid (resolved via GET /api/v1/meta/public-key), then runs the seven verification checks above against the expected audience (typically the app’s own hostname or service identifier).

For full custom logic (audience mapping, replay protection, per-route policies), write a proxy hook. The reference recipe, identity-claim-auth-gate, likewise uses the non-reserved x-hoody-claim header to receive the bundle. See the proxy hooks reference.

Mints a fresh, audience-bound identity claim for the authenticated caller without requiring a re-login. First-party JWT sessions only: auth tokens, HTTP Basic, and impersonated sessions are rejected (403). The claim proves “Hoody authenticated this user” to the audience named in the request; third parties verify it offline against GET /api/v1/meta/public-key. Claim lifetime is clamped to [60s, min(server ceiling — default 24h, remaining JWT lifetime)], with a default of 1 hour. When fewer than 60 seconds remain on the caller’s JWT, this endpoint returns 400 REFRESH_REQUIRED rather than minting a claim that would expire before it can be used. Two rate limits apply: the standard per-IP flood guard and a tighter per-session cap.

FieldTypeRequiredDescription
audiencestringYesConsumer identifier this claim is bound to (e.g. your app hostname). Verifiers reject the claim unless they expect exactly this audience. Printable ASCII, no whitespace or double quotes.
expires_inintegerNoRequested claim lifetime in seconds. Clamped to [60, min(server ceiling, remaining JWT lifetime)]. Default: server-configured (1 hour).
Terminal window
curl -X POST https://api.hoody.com/api/v1/users/auth/identity-claim \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \
-H 'Content-Type: application/json' \
-d '{
"audience": "myapp.example.com",
"expires_in": 3600
}'
{
"statusCode": 200,
"message": "Identity claim issued",
"data": {
"identity_claim": {
"kid": "v1",
"payload_b64": "eyJjbGFpbV90eXBlIjoiaWRlbnRpdHkiLCJpc3MiOiJob29keS1hcGkiLCJzdWIiOiI1MDdmMWY3N2JjZjg2Y2Q3OTk0MzkwMTEiLCJ1c2VybmFtZSI6ImpvaG5fZG9lIiwidHlwZSI6InVzZXIiLCJpYXQiOjE3NDEyOTAwMDAsImV4cCI6MTc0Mzg4MjAwMCwia2lkIjoidjEiLCJhdWQiOiJteWFwcC5leGFtcGxlLmNvbSJ9",
"signature_hex": "abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef12"
},
"expires_in": 3600,
"expires_at": "2026-01-15T11:30:00.000Z",
"audience": "myapp.example.com"
}
}

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.

{
"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
}
}
Terminal window
curl https://api.hoody.com/api/v1/users/auth/2fa/status \
-H 'Authorization: Bearer <jwt_token>'

Begin 2FA setup. Requires the current account password for verification. Returns the QR code for the authenticator app and backup codes. Important: save backup codes securely, they are shown only once.

FieldTypeRequiredDescription
passwordstringYesCurrent account password for verification.
Terminal window
curl -X POST https://api.hoody.com/api/v1/users/auth/2fa/setup \
-H 'Authorization: Bearer <jwt_token>' \
-H 'Content-Type: application/json' \
-d '{ "password": "SecurePassword123!" }'
{
"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"
]
}
}

Verify and complete 2FA setup by providing the first code from your authenticator app. This confirms the setup is working correctly. On success, all sessions are revoked and a fresh token / refreshToken pair is returned (the response carries data.token, data.refreshToken, and data.sessions_revoked: true).

FieldTypeRequiredDescription
codestringYes6-digit code from the authenticator app.
Terminal window
curl -X POST https://api.hoody.com/api/v1/users/auth/2fa/verify-setup \
-H 'Authorization: Bearer <jwt_token>' \
-H 'Content-Type: application/json' \
-d '{ "code": "123456" }'
{
"statusCode": 200,
"message": "2FA successfully enabled. All other sessions have been signed out.",
"data": {
"enabled": true,
"enabled_at": "2025-01-14T21:00:00.000Z",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"sessions_revoked": true
}
}

Complete login by verifying a 2FA code. Use the temp_token from the login response and provide either a 6-digit OTP code or a backup code. On success, the response includes an identity claim under data.identity_claim proving “Hoody authenticated this user”.

FieldTypeRequiredDescription
temp_tokenstringNoTemporary token from login response (valid for 5 minutes). Alternatively pass it as Authorization: Bearer header.
codestringYes6-digit OTP code from authenticator app OR 10-character backup code.
response_modestringNoResponse shape. tokens (default) returns access/refresh tokens. intent returns an opaque auth_intent_token for PKCE exchange.
clientstringNoOptional client-declared source channel for analytics.
Terminal window
curl -X POST https://api.hoody.com/api/v1/users/auth/2fa/verify \
-H 'Authorization: Bearer <temp_token>' \
-H 'Content-Type: application/json' \
-d '{ "code": "123456" }'
{
"statusCode": 200,
"message": "Authentication successful",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"identity_claim": {
"kid": "v1",
"payload_b64": "eyJjbGFpbV90eXBlIjoiaWRlbnRpdHkiLCJpc3MiOiJob29keS1hcGkiLCJzdWIiOiI1MDdmMWY3N2JjZjg2Y2Q3OTk0MzkwMTEiLCJ1c2VybmFtZSI6ImpvaG5fZG9lIiwidHlwZSI6InVzZXIiLCJpYXQiOjE3NDEyOTAwMDAsImV4cCI6MTc0Mzg4MjAwMCwia2lkIjoidjEifQ",
"signature_hex": "abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef12"
},
"user": {
"id": "507f1f77bcf86cd799439011",
"alias": "John Doe"
}
}
}

POST /api/v1/users/auth/2fa/backup-codes/regenerate

Section titled “POST /api/v1/users/auth/2fa/backup-codes/regenerate”

Generate new backup codes (invalidates all existing ones). Requires the password and current OTP code for security. Important: save the new codes securely!

FieldTypeRequiredDescription
passwordstringYesCurrent account password.
codestringYes6-digit OTP code from authenticator app.
Terminal window
curl -X POST https://api.hoody.com/api/v1/users/auth/2fa/backup-codes/regenerate \
-H 'Authorization: Bearer <jwt_token>' \
-H 'Content-Type: application/json' \
-d '{
"password": "SecurePassword123!",
"code": "123456"
}'
{
"statusCode": 200,
"message": "Backup codes regenerated",
"data": {
"backup_codes": [
"a1b2c3d4e5",
"f6g7h8i9j0",
"k1l2m3n4o5",
"p6q7r8s9t0",
"u1v2w3x4y5",
"z6a7b8c9d0",
"e1f2g3h4i5",
"j6k7l8m9n0",
"o1p2q3r4s5",
"t6u7v8w9x0"
]
}
}

Enable or disable the OTP requirement for token mutation operations. Disabling requires both password and OTP.

PATCH on the same path is also accepted and behaves identically.

The SDK reaches this operation through two methods:

  • client.api.auth.twoFactor.enableTokenGate() sends enabled: true.
  • client.api.auth.twoFactor.disableTokenGate({ password, otp_code }) sends enabled: false.
FieldTypeRequiredDescription
enabledbooleanYestrue to require OTP for token mutations (default), false to skip OTP gate.
passwordstringNoRequired when setting enabled=false (security downgrade requires primary-factor reauth).
otp_codestringNoTOTP code or backup code. Required when setting enabled=false.
Terminal window
curl -X PUT https://api.hoody.com/api/v1/users/auth/2fa/token-gate \
-H 'Authorization: Bearer <jwt_token>' \
-H 'Content-Type: application/json' \
-d '{
"enabled": true
}'
{
"statusCode": 200,
"message": "Token gate preference updated",
"data": { "require_for_tokens": true }
}

Disable 2FA for the account. Requires both current password and a valid OTP code (or backup code) for security. On success, all sessions are revoked and a fresh token / refreshToken pair is returned (the response carries data.token, data.refreshToken, and data.sessions_revoked: true).

FieldTypeRequiredDescription
passwordstringYesCurrent account password.
codestringYes6-digit OTP code from authenticator app OR backup code.
Terminal window
curl -X DELETE https://api.hoody.com/api/v1/users/auth/2fa \
-H 'Authorization: Bearer <jwt_token>' \
-H 'Content-Type: application/json' \
-d '{
"password": "SecurePassword123!",
"code": "123456"
}'
{
"statusCode": 200,
"message": "2FA successfully disabled. All other sessions have been signed out.",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"sessions_revoked": true
}
}