Skip to content
Hoody.com

The endpoints in this section cover everything you need to sign users in, mint and refresh session tokens, recover accounts, exchange OAuth codes (including the CLI/device flow), and protect sessions with two-factor authentication. They also expose the ED25519 identity claims that let third-party systems verify a Hoody login offline.

For container-side authentication, see Container claims. For proxy-side authentication of upstream calls, see the proxy foundation pages linked from each gating pattern under Identity claims.

Returns the regions where free-tier servers currently exist, with a boolean availability flag for each. Public, no authentication required.

Terminal window
curl https://api.hoody.com/api/v1/auth/available-regions

This endpoint takes no parameters.

{
"statusCode": 200,
"data": {
"regions": [
{ "region": "eu-west", "country": "Netherlands", "city": "Amsterdam", "available": true },
{ "region": "us-east", "country": "USA", "city": "Ashburn", "available": true },
{ "region": "ap-south", "country": "Singapore", "city": "Singapore", "available": false }
]
}
}

Returns the public sign-in configuration (which identity providers are enabled, and other UI-driving flags). Public, no authentication required.

Terminal window
curl https://api.hoody.com/api/v1/auth/config

This endpoint takes no parameters.

{}

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.

keys[] is a rotation array. It carries the next prepublished key plus the previous key retained for verification; always look up the key by kid. On key rotation, Hoody increments kid (for example v1 to v2) and includes both the active and the retired keys in the array for a transition window so existing signatures still verify. New signatures use active_kid.

Third-party verification flow:

  1. Fetch this endpoint once and cache the result for 24h or longer.
  2. Locate the key by kid from the keys[] array.
  3. For response signatures: parse the X-Hoody-Signature header in the form t=<unix_ts>,kid=<key_id>,m=<method>,s=<status>,path=<request_url>,sig=<hex>, then 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 (the base64url string itself).
  5. If a kid in a signature or claim does not match any cached key, re-fetch this endpoint.
Terminal window
curl https://api.hoody.com/api/v1/meta/public-key

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
}
}
}

An identity claim is an ED25519-signed credential that proves “Hoody authenticated this user” to systems outside Hoody. It is verified offline against the public key fetched from GET /api/v1/meta/public-key; no live call to Hoody is required.

Hoody returns an identity claim in these flows:

  • POST /api/v1/users/auth/login (tokens mode) — see Login
  • POST /api/v1/users/auth/2fa/verify (tokens mode, after the OTP step) — see Verify 2FA
  • POST /api/v1/auth/verify-email (tokens mode) — see Verify email
  • POST /api/v1/auth/device/token (terminal polling completes) — see Device token
  • The hosted auth UI PKCE exchange at POST /api/v1/auth/authorize and POST /api/v1/auth/exchange

The claim is omitted when the response mode is intent (hosted auth handoff returns an opaque auth_intent_token instead) and when signing is not configured on the server (HOODY_SIGNING_PRIVATE_KEY unset). If your flow needs a guarantee of issuance, force response_mode: "tokens" and treat a missing identity_claim as a configuration error.

FieldTypeDescription
kidstringKey ID. Look it up in the keys[] rotation array from GET /api/v1/meta/public-key.
payload_b64stringbase64url-encoded JSON payload (no padding). The signature is over the UTF-8 bytes of this string.
signature_hexstring128-character lowercase hex (64-byte) ED25519 detached signature.
FieldTypeDescription
claim_typestringAlways "identity".
issstringAlways "hoody-api".
substringThe authenticated user ID (24-character hex).
usernamestringThe user’s username at issue time.
typestring"user" or "admin".
iatnumberIssue time (Unix seconds).
expnumberExpiry time (Unix seconds). Default ~30 days for login claims; re-issued claims default to 1 hour.
kidstringKey ID; must match bundle.kid.
audstring(Re-issued claims only) The audience the claim is bound to. Strict, two-way match required by the verifier.

A verifier MUST run all seven checks below before trusting any field of an identity claim. Reject on first failure.

  1. Signature over UTF-8 bytes of payload_b64. Verify signature_hex against Buffer.from(payload_b64, 'utf8') (or equivalent) using the ED25519 public key located by bundle.kid in the keys[] rotation array. Never re-encode, re-stringify, or re-base64 the payload before verification.
  2. claim_type === "identity". Reject anything else; container claims and future claim types are not interchangeable.
  3. iss === "hoody-api". Reject any other issuer.
  4. exp > now and exp > iat. The claim must be unexpired and internally consistent.
  5. iat <= now + 300. Allow 5 minutes of clock skew; reject anything that claims to be issued in the future beyond that.
  6. payload.kid === bundle.kid. The key referenced inside the signed payload must match the key the signature was checked against.
  7. Audience strict, two-way match. A claim is bound to exactly one audience string in payload.aud. The verifier MUST reject the claim unless the audience the verifier expects is exactly equal to payload.aud. This is mandatory for every verifier, every time, regardless of whether the audience field looks “optional”.
import { createPublicKey, verify, createHash } from 'node:crypto';
// 1. Fetch and cache GET /api/v1/meta/public-key; locate the key by kid:
// data.keys.find(k => k.kid === claim.kid)?.public_key_hex
// 2. SPKI prefix for ED25519 public keys (12 bytes):
const SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
function verifyClaim(claim: { kid: string; payload_b64: string; signature_hex: string }, expectedAudience: string) {
const hex = '<' + /* public_key_hex from GET /api/v1/meta/public-key for claim.kid */ '>' as string;
const rawKey = Buffer.from(hex, 'hex');
const spki = Buffer.concat([SPKI_PREFIX, rawKey]);
const pubKey = createPublicKey({ key: spki, format: 'der', type: 'spki' });
const payload = Buffer.from(claim.payload_b64, 'utf8');
const sig = Buffer.from(claim.signature_hex, 'hex');
if (!verify(null, payload, pubKey, sig)) throw new Error('bad signature');
const body = JSON.parse(claim.payload_b64);
if (body.claim_type !== 'identity') throw new Error('wrong claim_type');
if (body.iss !== 'hoody-api') throw new Error('wrong iss');
const now = Math.floor(Date.now() / 1000);
if (!(body.exp > now && body.exp > body.iat)) throw new Error('expired');
if (!(body.iat <= now + 300)) throw new Error('iat in the future');
if (body.kid !== claim.kid) throw new Error('kid mismatch');
if (body.aud !== expectedAudience) throw new Error('audience mismatch');
return body; // trusted
}

Decodable example claim (illustrative):

{
"claim_type": "identity",
"iss": "hoody-api",
"sub": "67e89abc123def456789abcd",
"username": "alice",
"type": "user",
"iat": 1741290000,
"exp": 1743882000,
"kid": "v1",
"aud": "myapp.example.com"
}

There are three ways to forward an identity claim through the proxy so an upstream app can verify it. Pick the one that matches your threat model:

  1. Native hoody-identity permission group. Grant the hoody-identity group to the upstream app in proxy permissions; the edge forwards the verified claim to your app unmodified. See proxy permissions — hoody-identity.
  2. App-level via a NON-RESERVED header. Convention: X-Hoody-Claim: <payload_b64>.<signature_hex>, with the verifier pinning the expected kid (and the expected aud for audience-bound claims). The verifier runs the seven mandatory checks above before trusting any field.
  3. Proxy hook. A proxy_hook can inspect the request and inject the claim header on the upstream call after applying your own policy. See proxy hooks — identity-claim-auth-gate; the recipe likewise uses the non-reserved x-hoody-claim header.

For container-side use (a program running inside a Hoody container reading the claim), see Container claims — the format and verification are the same, but the delivery mechanism differs.

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 and the __Host-device_verify cookie atomically and redirects to the provider with a server-injected device binding and attempt nonce. Sets Referrer-Policy: no-referrer.

Terminal window
curl -i "https://api.hoody.com/api/v1/auth/device/authorize?ticket=0000000000000000000000000000000000000000000000000000000000000000&provider=github"
NameInTypeRequiredDescription
ticketquerystringYesdevice_verify_ticket from /device/verify_code
providerquerystringYesOne of "github", "google"
{}

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

Terminal window
curl -i "https://api.hoody.com/api/v1/auth/github?redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
NameInTypeRequiredDescription
clientquerystringNoSource channel for analytics: web, ssh, webssh, cli, sdk, agent. Unknown values recorded as unknown.
intentquerystringNoOAuth intent: login (default). star_check is accepted but ignored (retired).
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”). Hashed at redirect time; only the hash travels in OAuth state.
{}

Handles the GitHub OAuth callback. Browser-only endpoint.

Terminal window
curl -i "https://api.hoody.com/api/v1/auth/github/callback?state=<signed_state>&code=<oauth_code>"
NameInTypeRequiredDescription
codequerystringNoOAuth authorization code from GitHub.
statequerystringYesSigned OAuth state echoed from the redirect.
errorquerystringNoProvider-side failure code (e.g. access_denied). Present instead of code when the user declines.
error_descriptionquerystringNoProvider-supplied description.
error_uriquerystringNoProvider-supplied URI.
{}

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

Terminal window
curl -i "https://api.hoody.com/api/v1/auth/google?redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
NameInTypeRequiredDescription
clientquerystringNoSource channel for analytics: web, ssh, webssh, cli, sdk, agent. Unknown values 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).
invite_codequerystringNoOptional invite code (“coupon”). Hashed at redirect time.
{}

Handles the Google OAuth callback. Browser-only endpoint.

Terminal window
curl -i "https://api.hoody.com/api/v1/auth/google/callback?state=<signed_state>&code=<oauth_code>"
NameInTypeRequiredDescription
codequerystringNoOAuth authorization code from Google.
statequerystringYesSigned OAuth state echoed from the redirect.
errorquerystringNoProvider-side failure code (e.g. access_denied).
error_descriptionquerystringNoProvider-supplied description.
error_uriquerystringNoProvider-supplied URI.
{}

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

Terminal window
curl -i "https://api.hoody.com/api/v1/auth/launch/start?ticket=<launch_ticket>"
NameInTypeRequiredDescription
ticketquerystringYesOne-shot ticket from /launch/initiate response
{}

Registers a PKCE authorization request (code challenge and redirect URI) to begin the browser sign-in flow.

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/callback"
}'

This endpoint takes no parameters.

Request Body

NameTypeRequiredDescription
code_challengestringYesPKCE code challenge (base64url SHA-256 of code_verifier).
redirect_uristringYesFrontend URL to redirect to after authorization (must start with https://).
{}

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 not standards-compliant: no client_id/grant_type, lifecycle errors are nested under data, and the poll interval is a fixed 5s after slow_down.

Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/device/code \
-H 'Content-Type: application/json' \
-d '{
"client_name": "Hoody CLI",
"code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
}'

This endpoint takes no parameters.

Request Body

NameTypeRequiredDescription
client_namestringNoShown on the verification page as “X is requesting access”.
clientstringNoSource channel for analytics: web, ssh, webssh, cli, sdk, agent. Unknown values recorded as unknown.
code_challengestringNoPKCE on the device flow itself; if present, the poll REQUIRES the verifier.
{
"statusCode": 200,
"data": {
"device_code": "0000000000000000000000000000000000000000000000000000000000000000",
"user_code": "ABCD-1234",
"verification_uri": "https://app.example.com/device",
"verification_uri_complete": "https://app.example.com/device?user_code=ABCD-1234",
"interval": 5,
"expires_in": 600
}
}

Page-only helper. Mirrors the RFC-8628 recommendation that the user can deny. Cookie + ticket gated, no credentials required — possession of the live ticket and cookie is the refusing authority. Flips the pending row to denied; the terminal poll then reports access_denied. An approved row can never be un-approved.

Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/device/deny \
-H 'Content-Type: application/json' \
-d '{
"ticket": "0000000000000000000000000000000000000000000000000000000000000000"
}'

This endpoint takes no parameters.

Request Body

NameTypeRequiredDescription
ticketstringYesdevice_verify_ticket from /device/verify_code.
{
"statusCode": 200,
"data": { "status": "denied" }
}

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

Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/device/login \
-H 'Content-Type: application/json' \
-d '{
"ticket": "0000000000000000000000000000000000000000000000000000000000000000",
"username": "alice",
"password": "SecurePassword123!"
}'

This endpoint takes no parameters.

Request Body

NameTypeRequiredDescription
ticketstringYesdevice_verify_ticket from /device/verify_code.
usernamestringNoUsername (alternative to email).
emailstringNoEmail address (alternative to username).
passwordstringYesAccount password.
{
"statusCode": 200,
"data": {
"status": "approved"
}
}

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

Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/device/token \
-H 'Content-Type: application/json' \
-d '{
"device_code": "0000000000000000000000000000000000000000000000000000000000000000",
"code_verifier": "kJpQ6yIBl5R7yHnL4Yd2fMz3aW9eTv0cN1sXrUaVpBgGhTqOiAkDjFlVnCx"
}'

This endpoint takes no parameters.

Request Body

NameTypeRequiredDescription
device_codestringYesdevice_code from /device/code.
code_verifierstringNoPKCE code_verifier (required when code_challenge was set in /device/code).
{
"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": "eyJzdWIiOiI2N2U4OWFiYzEyM2RlZjQ1Njc4OWFiY2QiLCJ1c2VybmFtZSI6ImFsaWNlIiwidHlwZSI6InVzZXIiLCJpYXQiOjE3NDEyOTAwMDAsImV4cCI6MTc0Mzg4MjAwMCwia2lkIjoidjEifQ",
"signature_hex": "abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef12"
},
"user": {
"id": "67e89abc123def456789abcd",
"username": "alice",
"alias": "Alice",
"email": "alice@example.com",
"is_admin": false,
"email_verified": true,
"signup_method": "github",
"avatar_url": null,
"created_at": "2024-12-01T10:00:00.000Z",
"updated_at": "2025-01-15T10:30:00.000Z"
},
"server": {
"id": "67e89abc123def456789abcd",
"name": "node-us-01",
"country": "USA",
"region": "us-east",
"city": "Ashburn",
"datacenter": "iad1",
"is_ready": true
},
"project": {
"id": "67e89abc123def456789abcd",
"alias": "default"
},
"container": {
"id": "890abcdef12345678901cdef",
"name": "main",
"status": "running"
}
}
}

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.

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" }'

This endpoint takes no parameters.

Request Body

NameTypeRequiredDescription
user_codestringYesXXXX-XXXX user code (dashes optional).
{
"statusCode": 200,
"data": {
"client_name": "Hoody CLI",
"status": "pending"
}
}

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

Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/exchange \
-H 'Content-Type: application/json' \
-d '{
"code": "0000000000000000000000000000000000000000000000000000000000000000",
"code_verifier": "kJpQ6yIBl5R7yHnL4Yd2fMz3aW9eTv0cN1sXrUaVpBgGhTqOiAkDjFlVnCx",
"redirect_uri": "https://app.example.com/callback"
}'

This endpoint takes no parameters.

Request Body

NameTypeRequiredDescription
codestringYesAuthorization code returned by the OAuth callback.
code_verifierstringYesPKCE code_verifier (43-128 chars).
redirect_uristringYesFrontend URL to redirect to (must match the original authorize call).
{}

Cancels a pending OAuth AuthIntent or 2FA temp_token. Requires Authorization: Bearer <intent_or_temp_token>. Idempotent. Used by the handoff page when the user dismisses the confirmation.

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

This endpoint takes no parameters and no request body.

{}

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 and opener_origin plumbed through.

Terminal window
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": "67e89ab0-1234-4def-9abc-def4567890ab"
}'

This endpoint takes no parameters.

Request Body

NameTypeRequiredDescription
providerstringYesOne of "github", "google".
clientstringNoSource 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.
{
"statusCode": 200,
"data": {
"launch_url": "https://api.hoody.com/api/v1/auth/launch/start?ticket=<launch_ticket>"
}
}

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

Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/forgot-password \
-H 'Content-Type: application/json' \
-d '{ "email": "alice@example.com" }'

This endpoint takes no parameters.

Request Body

NameTypeRequiredDescription
emailstringYesEmail address associated with the account.
{
"statusCode": 200,
"message": "If an account exists for that email, a password reset link has been sent."
}

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

Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/resend-verification \
-H 'Content-Type: application/json' \
-d '{ "email": "alice@example.com" }'

This endpoint takes no parameters.

Request Body

NameTypeRequiredDescription
emailstringYesEmail address to resend verification to.
{
"statusCode": 200,
"message": "If the email is registered and unverified, a new verification link has been sent."
}

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

Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/reset-password \
-H 'Content-Type: application/json' \
-d '{
"token": "0000000000000000000000000000000000000000000000000000000000000000",
"password": "NewSecurePassword123!"
}'

This endpoint takes no parameters.

Request Body

NameTypeRequiredDescription
tokenstringYesPassword reset token from the email link.
passwordstringYesNew password (min 12 chars).
{
"statusCode": 200,
"message": "Password reset successful"
}

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

Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/signup \
-H 'Content-Type: application/json' \
-d '{
"email": "alice@example.com",
"password": "SecurePassword123!"
}'

This endpoint takes no parameters.

Request Body

NameTypeRequiredDescription
emailstringYesEmail address for the new account.
passwordstringYesPassword (min 12 chars, must include uppercase, lowercase, number, and special char).
regionstringNoOptional preferred server region (e.g. eu-west). If omitted, auto-assigned by GeoIP proximity.
invite_codestringNoOptional invite code (“coupon”). Memorized (hash-only) and applied automatically after email verification.
clientstringNoSource channel for analytics.
{
"statusCode": 200,
"message": "Account created. Check your email to verify.",
"data": { "email": "alice@example.com" }
}

Verifies the email address using the token from the verification email. Default response returns full login credentials. When response_mode=intent and 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 and temp_token instead. See Identity claims for the audience-bound re-issue flow.

Terminal window
curl -X POST https://api.hoody.com/api/v1/auth/verify-email \
-H 'Content-Type: application/json' \
-d '{
"token": "0000000000000000000000000000000000000000000000000000000000000000"
}'

This endpoint takes no parameters.

Request Body

NameTypeRequiredDescription
clientstringNoSource channel for analytics.
tokenstringYesVerification token from the email link.
response_modestringNo"intent" returns an auth_intent_token; "tokens" (default) returns access/refresh tokens.
code_challengestringNoPKCE code_challenge (base64url SHA-256 of code_verifier). Required when response_mode=intent.
{
"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": "eyJzdWIiOiI2N2U4OWFiYzEyM2RlZjQ1Njc4OWFiY2QiLCJ1c2VybmFtZSI6ImFsaWNlIiwidHlwZSI6InVzZXIiLCJpYXQiOjE3NDEyOTAwMDAsImV4cCI6MTc0Mzg4MjAwMCwia2lkIjoidjEifQ",
"signature_hex": "abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef12"
},
"user": {
"id": "67e89abc123def456789abcd",
"username": "alice",
"alias": "Alice",
"email": "alice@example.com",
"is_admin": false,
"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": "67e89abc123def456789abcd",
"name": "node-us-01",
"country": "USA",
"region": "us-east",
"city": "Ashburn",
"datacenter": "iad1",
"is_ready": true
},
"project": {
"id": "67e89abc123def456789abcd",
"alias": "default"
},
"container": {
"id": "890abcdef12345678901cdef",
"name": "main"
}
}
}

Authenticates with username and password and returns a JWT access token (1 day) and a refresh token (7 days). Use the access token in the Authorization header for subsequent requests: Authorization: Bearer <token>. See Identity claims for the optional audience-bound re-issue flow.

Terminal window
curl -X POST https://api.hoody.com/api/v1/users/auth/login \
-H 'Content-Type: application/json' \
-d '{
"username": "alice",
"password": "SecurePassword123!"
}'

This endpoint takes no parameters.

Request Body

NameTypeRequiredDescription
usernamestringNoUsername (alternative to email).
emailstringNoEmail address (alternative to username).
passwordstringYesAccount password.
response_modestringNo"intent" returns an auth_intent_token; "tokens" (default) returns access/refresh tokens.
clientstringNoSource channel for analytics.
code_challengestringNoPKCE code_challenge. Required when response_mode=intent.
{
"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": "eyJzdWIiOiI2N2U4OWFiYzEyM2RlZjQ1Njc4OWFiY2QiLCJ1c2VybmFtZSI6ImFsaWNlIiwidHlwZSI6InVzZXIiLCJpYXQiOjE3NDEyOTAwMDAsImV4cCI6MTc0Mzg4MjAwMCwia2lkIjoidjEifQ",
"signature_hex": "abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef12"
},
"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": "67e89abc123def456789abcd",
"username": "alice",
"alias": "Alice",
"email": "alice@example.com",
"is_admin": false,
"is_banned": false,
"email_verified": true,
"avatar_url": null,
"signup_method": "email",
"metadata": {},
"created_at": "2024-12-01T10:00:00.000Z",
"updated_at": "2025-01-15T10:30:00.000Z"
}
}
}

Logs out the current user and creates an audit log entry. Note: in a stateless JWT setup the client should discard the token. This endpoint works even for banned users.

Terminal window
curl -X POST https://api.hoody.com/api/v1/users/auth/logout \
-H 'Authorization: Bearer <token>'

This endpoint takes no parameters and no request body.

{
"statusCode": 200,
"message": "Logout successful"
}

Exchanges a valid refresh token for a new access token and new refresh token. Send the refresh token in the body.

Terminal window
curl -X POST https://api.hoody.com/api/v1/users/auth/refresh \
-H 'Content-Type: application/json' \
-d '{ "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }'

This endpoint takes no parameters.

Request Body

NameTypeRequiredDescription
refreshTokenstringYesValid refresh token from a previous login/refresh.
{
"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
}
}

Retrieves 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). 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.

Terminal window
curl https://api.hoody.com/api/v1/users/auth/me \
-H 'Authorization: Bearer <token>'

This endpoint takes no parameters.

{
"statusCode": 200,
"message": "Current user retrieved successfully",
"data": {
"id": "67e89abc123def456789abcd",
"username": "alice",
"alias": "Alice",
"email": "alice@example.com",
"is_admin": false,
"is_banned": false,
"email_verified": true,
"metadata": {},
"created_at": "2024-12-01T10:00:00.000Z",
"updated_at": "2025-01-15T10:30:00.000Z"
}
}

Alias of GET /api/v1/users/auth/me. Returns the same response.

Terminal window
curl https://api.hoody.com/api/v1/users/me \
-H 'Authorization: Bearer <token>'

This endpoint takes no parameters.

{
"statusCode": 200,
"message": "Current user retrieved successfully",
"data": {
"id": "67e89abc123def456789abcd",
"username": "alice",
"alias": "Alice",
"email": "alice@example.com",
"is_admin": false,
"is_banned": false,
"email_verified": true,
"metadata": {},
"created_at": "2024-12-01T10:00:00.000Z",
"updated_at": "2025-01-15T10:30:00.000Z"
}
}

Mints a fresh, audience-bound identity claim for the authenticated caller without a re-login. First-party JWT sessions only — auth tokens, HTTP Basic, and impersonated sessions are rejected. 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)]; default 1 hour.

The endpoint enforces two rate limits in parallel: a per-account limit (default 30/minute) and a global limit. Under 60 seconds of JWT remaining lifetime the call returns 400 REFRESH_REQUIRED — refresh the access token first.

Terminal window
curl -X POST https://api.hoody.com/api/v1/users/auth/identity-claim \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <jwt_token>' \
-d '{
"audience": "myapp.example.com",
"expires_in": 3600
}'

This endpoint takes no parameters.

Request Body

NameTypeRequiredDescription
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 (1h).
{
"statusCode": 200,
"message": "Identity claim issued",
"data": {
"identity_claim": {
"kid": "v1",
"payload_b64": "eyJzdWIiOiI2N2U4OWFiYzEyM2RlZjQ1Njc4OWFiY2QiLCJ1c2VybmFtZSI6ImFsaWNlIiwidHlwZSI6InVzZXIiLCJpYXQiOjE3NDEyOTAwMDAsImV4cCI6MTc0MTI5MzYwMCwia2lkIjoidjEiLCJhdWQiOiJteWFwcC5leGFtcGxlLmNvbSJ9",
"signature_hex": "abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef12"
},
"expires_in": 3600,
"expires_at": "2025-03-08T10:00:00.000Z",
"audience": "myapp.example.com"
}
}

Returns the current 2FA status for the authenticated user, including whether it is enabled and how many backup codes remain.

Terminal window
curl https://api.hoody.com/api/v1/users/auth/2fa/status \
-H 'Authorization: Bearer <token>'

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
}
}

Begins 2FA setup. Requires the current password for verification. Returns a QR code for an authenticator app and backup codes. The backup codes are shown only once — save them securely. On success, all sessions are revoked and a fresh token/refreshToken pair is returned so the calling client can adopt the new session without re-authenticating.

Terminal window
curl -X POST https://api.hoody.com/api/v1/users/auth/2fa/setup \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{ "password": "SecurePassword123!" }'

This endpoint takes no parameters.

Request Body

NameTypeRequiredDescription
passwordstringYesCurrent account password for verification.
{
"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"
]
}
}

Verifies and completes 2FA setup by providing the first code from your authenticator app. On success, all sessions are revoked and a fresh token/refreshToken pair is returned; sessions_revoked is always true.

Terminal window
curl -X POST https://api.hoody.com/api/v1/users/auth/2fa/verify-setup \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{ "code": "123456" }'

This endpoint takes no parameters.

Request Body

NameTypeRequiredDescription
codestringYes6-digit code from the authenticator app.
{
"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
}
}

Completes 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. See Identity claims for the optional audience-bound re-issue flow.

Terminal window
curl -X POST https://api.hoody.com/api/v1/users/auth/2fa/verify \
-H 'Content-Type: application/json' \
-d '{
"temp_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"code": "123456"
}'

This endpoint takes no parameters.

Request Body

NameTypeRequiredDescription
temp_tokenstringNoTemporary token from login response (5-minute TTL). Can also be sent as Authorization: Bearer.
codestringYes6-digit OTP code OR 10-character backup code
response_modestringNo"intent" returns an auth_intent_token; "tokens" (default) returns access/refresh tokens
clientstringNoSource channel for analytics
{
"statusCode": 200,
"message": "Authentication 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": "eyJzdWIiOiI2N2U4OWFiYzEyM2RlZjQ1Njc4OWFiY2QiLCJ1c2VybmFtZSI6ImFsaWNlIiwidHlwZSI6InVzZXIiLCJpYXQiOjE3NDEyOTAwMDAsImV4cCI6MTc0Mzg4MjAwMCwia2lkIjoidjEifQ",
"signature_hex": "abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef1234abcdef12"
}
}
}

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

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

Generates a fresh set of backup codes (invalidates all existing ones). Requires the current password and a current OTP code.

Terminal window
curl -X POST https://api.hoody.com/api/v1/users/auth/2fa/backup-codes/regenerate \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{
"password": "SecurePassword123!",
"code": "123456"
}'

This endpoint takes no parameters.

Request Body

NameTypeRequiredDescription
passwordstringYesCurrent account password
codestringYes6-digit OTP code from the authenticator app
{
"statusCode": 200,
"message": "Backup codes regenerated",
"data": {
"backup_codes": [
"a1b2c3d4e5", "f6g7h8i9j0", "k1l2m3n4o5", "p6q7r8s9t0", "u1v2w3x4y5",
"z6a7b8c9d0", "e1f2g3h4i5", "j6k7l8m9n0", "o1p2q3r4s5", "t6u7v8w9x0"
]
}
}

Enables or disables the OTP requirement for token mutation operations. Setting enabled=false (a security downgrade) requires both password and otp_code for primary-factor re-authentication. Note: the HTTP verb is PUT; the Patch suffix in the operationId is a naming artifact, not the method.

Terminal window
curl -X PUT https://api.hoody.com/api/v1/users/auth/2fa/token-gate \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{ "enabled": true }'

This endpoint takes no parameters.

Request Body

NameTypeRequiredDescription
enabledbooleanYestrue = require OTP for token mutations (default); false = skip the OTP gate
passwordstringNoRequired when enabled=false (security downgrade requires primary-factor reauth)
otp_codestringNoTOTP code or backup code. Required when enabled=false.
{
"statusCode": 200,
"message": "Token gate preference updated",
"data": {
"require_for_tokens": true
}
}

Disables 2FA for the account. Requires both the current password and a valid OTP code (or backup code). On success, all sessions are revoked and a fresh token/refreshToken pair is returned so the calling client can adopt the new session without re-authenticating. sessions_revoked is always true.

Terminal window
curl -X DELETE https://api.hoody.com/api/v1/users/auth/2fa \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{
"password": "SecurePassword123!",
"code": "123456"
}'

This endpoint takes no parameters.

Request Body

NameTypeRequiredDescription
passwordstringYesCurrent account password
codestringYes6-digit OTP code OR backup code
{
"statusCode": 200,
"message": "2FA successfully disabled. All other sessions have been signed out.",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"sessions_revoked": true
}
}