Skip to content
Hoody.com

Nearly all Hoody API operations require authentication. Only a few endpoints are public, such as GET /api/v1/notifications/public. There are two authentication systems, one for interactive user sessions and one for automation.

For what the API does (platform management), see the Hoody API overview.


This Foundation page explains how authentication works and how to use it safely. For the complete endpoint reference:

User authentication (JWT tokens):

Automation (Auth Tokens):


JWT tokens (user sessions)

Use for: browser sessions, interactive work

Terminal window
POST /api/v1/users/auth/login

Returns:

  • token (access, 1 day)
  • refreshToken (7 days)

Characteristics:

  • Short-lived
  • Refreshable without re-login
  • Tied to one user account
  • Not suited to automation

Auth Tokens (automation)

Use for: scripts, AI agents, CI/CD, integrations

Terminal window
POST /api/v1/auth/tokens

Returns:

  • hdy_... token (long-lived)

Characteristics:

  • Long-lived, with a configurable expiry
  • IP whitelist support
  • Revocable at any time
  • Per-token permissions
  • Suited to automation

Step 1: Log in

Terminal window
# Login with email and password (or use --username instead of --email)
hoody auth login --email you@example.com --password your_password
POST Login with username and password
/api/v1/users/auth/login
Click "Run" to execute the request

Response:

{
"statusCode": 200,
"message": "Login successful",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "63f8b0e5c9a1b2d3e4f5a6b7",
"username": "your_username",
"alias": "Your Display Name",
"is_banned": false,
"created_at": "2025-10-21T10:00:00.000Z",
"updated_at": "2025-10-21T10:00:00.000Z"
}
}
}

Step 2: Use the access token

Terminal window
# CLI stores the token automatically after login
hoody projects list

Step 3: Refresh when the token expires

Terminal window
# CLI handles token refresh automatically
# If your session expired, simply re-login
hoody auth login --username your_username --password your_password

The refresh token must be sent both in the Authorization: Bearer header and in the refreshToken body field. The server verifies the header token and rejects the request with 401 Invalid refresh token if the two do not match byte for byte.

Returns: a new access token and a new refresh token, both rotated.


Step 1: Create an Auth Token (one-time setup)

Terminal window
# Login first
hoody auth login --username your_username --password your_password
# Create a long-lived automation token with IP whitelist
hoody auth create \
--alias "Production Automation Token" \
--ip-whitelist "203.0.113.10,203.0.113.20" \
--expires-at "2027-04-12T00:00:00Z"
POST Create a long-lived automation token with IP whitelist
/api/v1/auth/tokens
Click "Run" to execute the request

Response:

{
"statusCode": 201,
"message": "Auth token created successfully",
"data": {
"token": "hdy_abc123XyZ456...",
"id": "63f8b0e5c9a1b2d3e4f5a6b7",
"alias": "Production Automation Token",
"prefix": "hdy_",
"ip_whitelist": ["203.0.113.10", "203.0.113.20"],
"expires_at": "2027-04-12T00:00:00.000Z",
"is_enabled": true,
"last_used_at": null,
"last_used_ip": null,
"created_at": "2025-11-09T15:00:00.000Z",
"updated_at": "2025-11-09T15:00:00.000Z"
}
}

Step 2: Use the Auth Token (until it expires or you revoke it)

Terminal window
# Store token and use with CLI
export HOODY_TOKEN="hdy_abc123XyZ456..."
# All subsequent commands use this token
hoody projects list
hoody containers list

An Auth Token gives you:

  • Credentials that live outside your code
  • IP whitelist enforcement, restricting use to specific addresses
  • Expiration control: an ISO 8601 date, or never
  • Instant revocation, by disabling or deleting the token
  • An audit trail in last_used_at and last_used_ip

alias accepts letters, digits, spaces, _ and - only (1 to 254 characters). Slashes and parentheses are rejected with 400.

IP whitelisting:

POST Create token with IP whitelist for CI/CD pipeline
/api/v1/auth/tokens
Click "Run" to execute the request

expires_at accepts several formats:

{
"expires_at": "2026-12-31T23:59:59Z"
}
Terminal window
# List all your auth tokens
hoody auth list
GET List all your auth tokens
/api/v1/auth/tokens
Click "Run" to execute the request

The response shows usage tracking:

{
"data": [
{
"id": "63f8b0e5c9a1b2d3e4f5a6b7",
"alias": "CI-CD Pipeline",
"prefix": "hdy_",
"ip_whitelist": ["203.0.113.50"],
"expires_at": "2026-02-07T15:00:00.000Z",
"is_enabled": true,
"last_used_at": "2025-11-09T14:30:00.000Z",
"last_used_ip": "203.0.113.50",
"created_at": "2025-11-09T10:00:00.000Z",
"updated_at": "2025-11-09T14:30:00.000Z"
}
]
}

Audit your tokens:

  • Check last_used_at to identify unused tokens
  • Verify last_used_ip matches expected sources
  • Review ip_whitelist restrictions

Disable without deleting:

Terminal window
# The CLI cannot disable tokens: '--is-enabled' is presence-only and always sets true.
# Use the SDK or HTTP tab to disable. To re-enable a disabled token:
hoody auth update $TOKEN_ID --is-enabled
PATCH Disable a token without deleting it
/api/v1/auth/tokens/{id}
Click "Run" to execute the request

Permanently delete:

Terminal window
# Permanently delete a token
hoody auth delete $TOKEN_ID
DELETE Permanently delete a token
/api/v1/auth/tokens/{id}
Click "Run" to execute the request

// Do not do this
const response = await fetch('https://api.hoody.com/api/v1/projects/', {
headers: {
'Authorization': 'Bearer hdy_abc123hardcoded'
}
});

Restrict a token to specific sources:

POST Create token that only works from your office IP
/api/v1/auth/tokens
Click "Run" to execute the request

If the token leaks, it will not work from any other IP.

Short-lived, for temporary access:

{
"alias": "Contractor Access",
"expires_at": "2026-05-12T00:00:00Z"
}

Long-lived, for permanent infrastructure:

{
"alias": "Production Services",
"expires_at": "2027-04-12T00:00:00Z"
}

Review and rotate tokens regularly.

Do not share a single token across systems:

Terminal window
# Create separate tokens
POST /api/v1/auth/tokens { "alias": "GitHub Actions CI", ... }
POST /api/v1/auth/tokens { "alias": "Monitoring System", ... }
POST /api/v1/auth/tokens { "alias": "AI Agent Orchestrator", ... }

If one service is compromised, revoke only that token; the others keep working.

Every successful sign-in to your account is recorded: the IP address, the country it resolved to, the client channel, and the timestamp. GET /api/v1/users/me/security-history returns your own trail, newest first. Pass include_failed=true to add rejected attempts against your account, and include_security=true to add other security events such as 2FA changes, password resets, and token creation or revocation. A burst of failed attempts, or a sign-in from a country you have never visited, is the clearest sign someone else is trying to get in.

GET List your sign-in and security-event history
/api/v1/users/me/security-history
Click "Run" to execute the request

The trail is append-only and kept for 180 days: you cannot delete entries, and neither can anyone who gets into your account. What is recorded, what is not, and how long it is kept is covered in Auditing & Data Collection.


The Auth Token system is built for AI orchestration:

// AI agent configuration (environment variables)
const HOODY_TOKEN = process.env.HOODY_TOKEN; // hdy_... token
const HOODY_API = 'https://api.hoody.com';
// AI can now orchestrate infrastructure
async function aiAgentWorkflow(task) {
const headers = {
'Authorization': `Bearer ${HOODY_TOKEN}`,
'Content-Type': 'application/json'
};
// AI decides: "Need a container to process this task"
const container = await fetch(`${HOODY_API}/api/v1/projects/${projectId}/containers`, {
method: 'POST',
headers,
body: JSON.stringify({
name: `ai-task-${Date.now()}`,
server_id: 'your-server-id',
hoody_kit: true
})
}).then(r => r.json());
// Wait for container to be running
await waitForStatus(container.data.id, 'running');
// Get container URLs and use them
const terminalUrl = `https://${projectId}-${container.data.id}-terminal-1.${container.data.server_name}.containers.hoody.com`;
// AI executes commands in the new container
await fetch(`${terminalUrl}/api/v1/terminal/execute`, {
method: 'POST',
body: JSON.stringify({ command: task.command })
});
// AI snapshots when done
await fetch(`${HOODY_API}/api/v1/containers/${container.data.id}/snapshots`, {
method: 'POST',
headers,
body: JSON.stringify({ alias: `task-${task.id}` })
});
return container;
}

The agent needs two things:

  1. A Hoody Auth Token in an environment variable
  2. An understanding of HTTP, which a model already has

There is no SDK to install and no training step. The interface is HTTP.


FeatureJWT (Login)Auth Token (hdy_…)
Lifetime1 day (access)
7 days (refresh)
Configurable (ISO 8601 date, “today”, “tomorrow”, or forever)
Use caseUser sessionsAutomation, AI, scripts
RefreshYes (via refresh token)No (create a new one when expired)
IP whitelistNoYes (optional)
RevocationLogout endpointDelete or disable
VisibilityManaged by browserShown once, at creation
SecurityShort lifetime limits exposureLong lifetime, bounded by IP whitelist

Terminal window
# 1. Login
LOGIN_RESPONSE=$(curl -X POST "https://api.hoody.com/api/v1/users/auth/login" \
-H "Content-Type: application/json" \
-d '{
"username": "dev_user",
"password": "strong_password_here"
}')
# Response includes tokens
# {
# "data": {
# "token": "eyJhbG...",
# "refreshToken": "eyJhbG...",
# "user": { ... }
# }
# }
# Capture the access and refresh tokens for steps 2-4
ACCESS_TOKEN=$(printf '%s' "$LOGIN_RESPONSE" | jq -r '.data.token')
REFRESH_TOKEN=$(printf '%s' "$LOGIN_RESPONSE" | jq -r '.data.refreshToken')
# 2. Use access token (valid 1 day)
curl "https://api.hoody.com/api/v1/projects/" \
-H "Authorization: Bearer $ACCESS_TOKEN"
# 3. Refresh before expiration (within 7 days)
# The refresh token goes in both the header and the body, and the two must match
curl -X POST "https://api.hoody.com/api/v1/users/auth/refresh" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $REFRESH_TOKEN" \
-d '{"refreshToken": "'"$REFRESH_TOKEN"'"}'
# 4. Logout (optional, invalidates session)
curl -X POST "https://api.hoody.com/api/v1/users/auth/logout" \
-H "Authorization: Bearer $ACCESS_TOKEN"
Terminal window
# 1. Login to get JWT
curl -X POST "https://api.hoody.com/api/v1/users/auth/login" \
-d '{"username": "your_username", "password": "your_password"}' \
> login.json
# Extract JWT
JWT=$(cat login.json | jq -r '.data.token')
# 2. Create auth token for CI/CD
curl -X POST "https://api.hoody.com/api/v1/auth/tokens" \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"alias": "GitHub Actions Deployment",
"ip_whitelist": ["140.82.112.0/20"],
"expires_at": "2027-04-12T00:00:00Z"
}' > token.json
# Extract auth token
AUTH_TOKEN=$(cat token.json | jq -r '.data.token')
# 3. Save to GitHub Secrets as HOODY_TOKEN
echo "HOODY_TOKEN=$AUTH_TOKEN"
# 4. Use in GitHub Actions workflow
# - name: Deploy via Hoody API
# env:
# HOODY_TOKEN: ${{ secrets.HOODY_TOKEN }}
# run: |
# curl "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/start" \
# -H "Authorization: Bearer $HOODY_TOKEN"
.env
// AI agent configuration file
HOODY_TOKEN=hdy_abc123def456...
HOODY_PROJECT_ID=63f8b0e5c9a1b2d3e4f5a6b7
// agent.js
import 'dotenv/config';
class HoodyAgent {
constructor() {
this.token = process.env.HOODY_TOKEN;
this.projectId = process.env.HOODY_PROJECT_ID;
this.api = 'https://api.hoody.com';
}
async callAPI(endpoint, options = {}) {
return fetch(`${this.api}${endpoint}`, {
...options,
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json',
...options.headers
}
});
}
async spawnContainer(name, config) {
const response = await this.callAPI(
`/api/v1/projects/${this.projectId}/containers`,
{
method: 'POST',
body: JSON.stringify({
name,
server_id: config.serverId,
hoody_kit: true,
...config
})
}
);
return response.json();
}
async executeInContainer(containerId, command) {
// First get container details to construct service URL
const container = await this.callAPI(`/api/v1/containers/${containerId}`)
.then(r => r.json());
// Construct terminal URL
const terminalUrl = `https://${this.projectId}-${containerId}-terminal-1.${container.data.server_name}.containers.hoody.com`;
// Execute command (no auth needed if container permissions are open)
return fetch(`${terminalUrl}/api/v1/terminal/execute`, {
method: 'POST',
body: JSON.stringify({ command })
});
}
}
// AI uses this class for every Hoody operation
const agent = new HoodyAgent();
await agent.spawnContainer('ai-workspace', { serverId: 'server-123' });

The agent only needs environment variables. It never handles a password or manages credentials.


GET List all your auth tokens
/api/v1/auth/tokens
Click "Run" to execute the request

Change the IP whitelist:

PATCH Update token IP whitelist
/api/v1/auth/tokens/{id}
Click "Run" to execute the request

Extend the expiration:

PATCH Extend token expiration
/api/v1/auth/tokens/{id}
Click "Run" to execute the request

Temporarily disable:

PATCH Temporarily disable a token
/api/v1/auth/tokens/{id}
Click "Run" to execute the request

Production rotation:

Terminal window
# 1. Create new token
NEW_TOKEN=$(curl -X POST "https://api.hoody.com/api/v1/auth/tokens" \
-H "Authorization: Bearer YOUR_JWT" \
-d '{"alias": "Production V2", "expires_at": "2027-04-12T00:00:00Z"}' \
| jq -r '.data.token')
# 2. Update your services with new token
# (Deploy new environment variable to all services)
# 3. Wait 24-48 hours for old token usage to drop
# 4. Check old token is unused
curl "https://api.hoody.com/api/v1/auth/tokens/{old_token_id}" \
-H "Authorization: Bearer YOUR_JWT" \
| jq '.data.last_used_at'
# 5. Delete old token
curl -X DELETE "https://api.hoody.com/api/v1/auth/tokens/{old_token_id}" \
-H "Authorization: Bearer YOUR_JWT"

For one-time operations:

POST Create a temporary token that auto-expires tomorrow
/api/v1/auth/tokens
Click "Run" to execute the request

Use the token for your migration, and it expires on its own tomorrow.

A different token for each environment:

Development token (permissive):

POST Create permissive development token
/api/v1/auth/tokens
Click "Run" to execute the request

Staging token (IP-restricted):

POST Create IP-restricted staging token
/api/v1/auth/tokens
Click "Run" to execute the request

Production token (strict):

POST Create strict production token
/api/v1/auth/tokens
Click "Run" to execute the request

If a token is compromised:

1. Disable it immediately:

PATCH Immediately disable compromised token
/api/v1/auth/tokens/{id}
Click "Run" to execute the request

2. Create a replacement with a different IP whitelist:

POST Create replacement token with new IP restrictions
/api/v1/auth/tokens
Click "Run" to execute the request

3. Update your services, then delete the old token:

DELETE Delete the compromised token after services are updated
/api/v1/auth/tokens/{id}
Click "Run" to execute the request

Should I use JWT tokens or Auth Tokens for my scripts?

Section titled “Should I use JWT tokens or Auth Tokens for my scripts?”

Use Auth Tokens (hdy_...) for scripts and automation. JWTs from login are meant for short-lived user sessions and expire after 1 day. An Auth Token can live for years, with IP whitelisting and revocation.

Can I create an Auth Token using another Auth Token?

Section titled “Can I create an Auth Token using another Auth Token?”

Yes, if the parent token holds the resources.create_tokens permission. Such a token can mint child tokens whose permissions are clamped to a subset of its own (each child’s delegation_depth is the parent’s plus one; 0 means account-minted from a JWT). Tokens without resources.create_tokens cannot create more tokens, and that includes any token created with deny_reauthorization: true, which strips the permission. Leave resources.create_tokens off for leaf tokens you hand to scripts.

How do I give my team API access without passwords?

Section titled “How do I give my team API access without passwords?”

Create individual Auth Tokens for each team member with specific IP whitelists. Each person gets their own hdy_... token, and you can revoke any token independently if someone leaves the team.

How do I check whether someone else accessed my account?

Section titled “How do I check whether someone else accessed my account?”

Call GET /api/v1/users/me/security-history. Each entry carries the event, a success or failed outcome, the IP address, the resolved country (null until resolved, and null permanently for addresses that cannot be located), the source channel, and the timestamp. Pass include_failed=true to include rejected sign-in attempts; those are only recorded when a credential was actually checked against your account, so the endpoint cannot be used to probe whether an account exists. For Auth Tokens, last_used_at and last_used_ip give the same signal per token.

What happens when my JWT access token expires?

Section titled “What happens when my JWT access token expires?”

After 1 day, the access token expires. Use your refresh token (valid 7 days) to get a new access token via POST /api/v1/users/auth/refresh. If the refresh token also expires, log in again with username and password.

Can one Auth Token cover several servers or apps?

Section titled “Can one Auth Token cover several servers or apps?”

Yes, but treat it as a convenience rather than a practice to standardize on. One token can work across several projects and servers; separate tokens per app or environment isolate better.

For realm-restricted tokens:

  • If realm_ids is non-empty (or allow_no_realm: false), use realm-scoped hosts like https://{realmId}.api.hoody.com.
  • Use GET /api/v1/auth/tokens/me on https://api.hoody.com to discover allowed realms before selecting a realm host.

How secure are Auth Tokens with no IP whitelist?

Section titled “How secure are Auth Tokens with no IP whitelist?”

Without an IP whitelist, a leaked token works from anywhere. The token itself is cryptographically strong (a long random string), but the whitelist adds defense-in-depth. Use it for production tokens and skip it for low-sensitivity automation.

Can Auth Tokens expire while my script is running?

Section titled “Can Auth Tokens expire while my script is running?”

Yes. If a long-running script spans the expiration time, it starts getting 401 errors. For long processes, use a far-future ISO 8601 date, omit expires_at at creation so the token never expires, or add logic that creates a replacement token before the old one expires.

What is the difference between disable and delete?

Section titled “What is the difference between disable and delete?”

Disabling sets is_enabled: false. The token stops working, and you can re-enable it later under the same ID. Deleting removes the token permanently, and it cannot be recovered. Use disable for a temporary suspension and delete for permanent revocation.

Can I use Auth Tokens with realm-scoped APIs?

Section titled “Can I use Auth Tokens with realm-scoped APIs?”

Yes. Auth Tokens work with realm-scoped APIs ({realmId}.api.hoody.com), and unrestricted tokens can also use the base API host.

Important behavior:

  • Tokens with non-empty realm_ids are restricted to those realm IDs.
  • Tokens with allow_no_realm: false cannot use the base host for resource operations.
  • Realm-restricted tokens can still call GET /api/v1/auth/tokens/me on the base host to bootstrap realm discovery.

How do I rotate Auth Tokens for zero-downtime updates?

Section titled “How do I rotate Auth Tokens for zero-downtime updates?”

Create the new token, deploy it to your services, verify it works, wait 24-48 hours, check that the old token’s last_used_at has stopped moving, then delete the old token. Both tokens work at the same time during the transition.


Problem: login returns 401 with “Invalid credentials”

Solutions:

  1. Verify username/password:
POST Check for typos, ensure exact match
/api/v1/users/auth/login
Click "Run" to execute the request
  1. Check account status:
    • The account may be banned (is_banned: true)
    • Contact support if the account should be active

Problem: requests return 401 after some time

Cause: the access token expires after 1 day

Solution: Use the refresh token.

Terminal window
# The refresh token goes in both the header and the body, and the two must match
curl -X POST "https://api.hoody.com/api/v1/users/auth/refresh" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $REFRESH_TOKEN" \
-d '{"refreshToken": "'"$REFRESH_TOKEN"'"}'

This returns a new access token and a new refresh token. If the refresh token has also expired, after 7 days, log in again.

Problem: an hdy_... token returns 403 Forbidden

Check IP whitelist:

GET Get token details to check IP whitelist
/api/v1/auth/tokens/{id}
Click "Run" to execute the request

Compare the ip_whitelist array with your current IP (run curl https://ifconfig.me in terminal).

Solution: Update the whitelist.

PATCH Update token IP whitelist to include your current IP
/api/v1/auth/tokens/{id}
Click "Run" to execute the request

Problem: creating an Auth Token with another Auth Token returns 403 Auth tokens cannot manage auth tokens

Cause: the calling token does not hold the resources.create_tokens permission. Tokens created with deny_reauthorization: true never hold it.

Solution: Grant resources.create_tokens when you create the parent token, or mint the child from a JWT obtained via login:

Terminal window
# 1. Login first
curl -X POST "https://api.hoody.com/api/v1/users/auth/login" \
-d '{"username": "your_username", "password": "your_password"}' \
> login.json
# 2. Extract JWT
JWT=$(cat login.json | jq -r '.data.token')
# 3. Create Auth Token with JWT
curl -X POST "https://api.hoody.com/api/v1/auth/tokens" \
-H "Authorization: Bearer $JWT" \
-d '{"alias": "My Token", "expires_at": "2027-04-12T00:00:00Z"}'

Problem: the token was created but the hdy_... value was not saved

Reality: the token value cannot be retrieved after creation

Solution:

  1. Disable the old token:
PATCH Disable the old token
/api/v1/auth/tokens/{id}
Click "Run" to execute the request
  1. Create a new token:
POST Create a replacement token, and save the token value immediately
/api/v1/auth/tokens
Click "Run" to execute the request

Problem: scripts work sometimes and fail other times with 403

Likely cause: an IP whitelist combined with a dynamic IP

Check if your IP changed:

Run curl https://ifconfig.me in a terminal to get your current IP, then compare it with the token whitelist:

GET Get token details to check IP whitelist
/api/v1/auth/tokens/{id}
Click "Run" to execute the request

Solutions:

  1. Use a CIDR range instead of a single IP: Instead of "203.0.113.50/32", use "203.0.113.0/24", which allows the whole subnet

  2. Remove the IP whitelist for non-sensitive automation:

PATCH Remove IP whitelist restrictions
/api/v1/auth/tokens/{id}
Click "Run" to execute the request

To lift IP restrictions, set ip_whitelist to ["*"], which allows all. An empty array is rejected with 400, because the array form requires at least one entry.

  1. Use a static IP for automation servers

  1. Create Projects - Organize your containers
  2. Spawn Containers - Create your first HTTP computer
  3. Configure Networking - Set up routing and firewall
  4. Create Proxy Aliases - Get clean URLs for production