Servers
Section titled “Servers”The Servers API covers the full rental lifecycle: browsing the marketplace of in-stock servers and orderable offers, reserving or renting, extending, monitoring live runtime, and managing team pools, invitations, and membership. Use these endpoints when you want to programmatically provision dedicated hardware, manage who shares access via pools, or watch resource consumption on rented boxes. It also covers the paid subserver subscription program: monthly plans you can buy, pay, upgrade, cancel, and renew.
Marketplace
Section titled “Marketplace”Browse in-stock servers and orderable offers. In-stock machines report delivery_hours: 0; offers carry a 1 to 720 hour lead time.
GET /api/v1/servers/available
Section titled “GET /api/v1/servers/available”Browse the rental marketplace. Returns in-stock servers with full pricing tiers and specifications.
curl -X GET 'https://api.hoody.com/api/v1/servers/available?country=US&min_cpu_cores=8&min_ram_gb=32' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
for await (const item of client.api.servers.listMarketplaceIterator({ country: 'US', min_cpu_cores: 8, min_ram_gb: 32})) { console.log(item);}{ "statusCode": 200, "message": "Available servers retrieved successfully", "data": [ { "id": "507f1f77bcf86cd799439014", "name": "node-example-1", "country": "US", "region": "us-east", "city": "New York", "datacenter": "NYC-DC1", "model": "Intel Xeon E5-2680 v4", "is_vm": false, "category": "compute", "featured": true, "popularity_rank": 1, "setup_time_minutes": 15, "delivery_hours": 0, "pricing": { "prices": { "1": "12.00", "7": "70.00", "30": "250.00" }, "setup_fee": "25.00", "setup_fee_cents": 2500, "price_tiers": { "1": { "price": "12.00", "hold_days": 1, "total_first_payment": "37.00" }, "7": { "price": "70.00", "hold_days": 2, "total_first_payment": "95.00" } } }, "specs": { "cpu": { "model": "AMD EPYC 7763", "cores": 64, "threads": 128, "score": 48500, "score_type": "passmark" }, "ram": { "capacity_gb": 256, "type": "ECC DDR5", "speed_mhz": 4800 }, "disks": { "config": [ { "count": 2, "capacity_gb": 1000, "type": "NVMe", "interface": "PCIe 4.0" }, { "count": 6, "capacity_gb": 15000, "type": "NVMe", "interface": "PCIe 4.0" } ], "total_gb": 92000, "summary": "2x1TB NVMe + 6x15TB NVMe" }, "network": { "bandwidth_mbps": 10000, "bandwidth_formatted": "10 Gbps", "traffic_tb": 100, "traffic_unlimited": false }, "additional": { "ipv4_count": 5, "ipv6_enabled": true } } } ]}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
country | query | string | No | Filter by country code (e.g., US, DE) |
region | query | string | No | Filter by region (e.g., us-east, eu-central) |
max_price_per_day | query | number | No | Maximum price per day in USD |
available_durations | query | array | No | Filter servers that support these rental durations (days) |
min_cpu_cores | query | number | No | Minimum CPU cores |
min_cpu_score | query | number | No | Minimum CPU benchmark score |
cpu_score_type | query | string | No | CPU benchmark type for score filtering. Allowed: passmark, geekbench_single, geekbench_multi |
min_ram_gb | query | number | No | Minimum RAM in GB |
ram_types | query | array | No | Filter by RAM types |
min_total_storage_gb | query | number | No | Minimum total storage in GB |
disk_types | query | array | No | Filter servers with these disk types |
min_bandwidth_mbps | query | number | No | Minimum network bandwidth in Mbps |
min_traffic_tb | query | number | No | Minimum monthly traffic allowance in TB |
unlimited_traffic_only | query | boolean | No | Show only servers with unlimited traffic |
category | query | string | No | Filter by server category. Allowed: compute, memory, storage, general, gpu |
featured_only | query | boolean | No | Show only featured servers |
Responses
Section titled “Responses”Returns the list of available servers matching the filters.
{ "statusCode": 200, "message": "Available servers retrieved successfully", "data": [ { "id": "507f1f77bcf86cd799439014", "name": "node-example-1", "country": "US", "region": "us-east", "city": "New York", "datacenter": "NYC-DC1", "model": "Intel Xeon E5-2680 v4", "is_vm": false, "category": "compute", "featured": true, "popularity_rank": 1, "setup_time_minutes": 15, "delivery_hours": 0, "pricing": { "prices": {}, "setup_fee": "25.00", "setup_fee_cents": 2500, "price_tiers": {} }, "specs": { "cpu": { "model": "AMD EPYC 7763", "cores": 64, "threads": 128, "score": 48500, "score_type": "passmark" }, "ram": { "capacity_gb": 256, "type": "ECC DDR5", "speed_mhz": 4800 }, "disks": { "config": [ { "count": 2, "capacity_gb": 1000, "type": "NVMe", "interface": "PCIe 4.0" }, { "count": 6, "capacity_gb": 15000, "type": "NVMe", "interface": "PCIe 4.0" } ], "total_gb": 92000, "summary": "2x1TB NVMe + 6x15TB NVMe" }, "network": { "bandwidth_mbps": 10000, "bandwidth_formatted": "10 Gbps", "traffic_tb": 100, "traffic_unlimited": false }, "additional": { "ipv4_count": 5, "ipv6_enabled": true } } } ]}Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}GET /api/v1/offers
Section titled “GET /api/v1/offers”Browse machines available to order with a delivery lead time. Offers report delivery_hours between 1 and 720; in-stock servers belong on /api/v1/servers/available instead.
curl -X GET 'https://api.hoody.com/api/v1/offers' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.servers.offers.list();{ "statusCode": 200, "message": "Offers retrieved successfully", "data": [ { "id": "507f1f77bcf86cd799439050", "name": "node-example-1", "country": "DE", "region": "eu-central", "city": "Frankfurt", "datacenter": "FRA-DC2", "cpu_model": "AMD EPYC 7402P", "cpu_cores": 24, "cpu_threads": 48, "ram_gb": 128, "disks": [ { "count": 2, "capacity_gb": 960, "type": "NVMe", "interface": "PCIe 4.0", "model": "Samsung PM983" } ], "bandwidth_mbps": 1000, "traffic_tb": 50, "traffic_unlimited": false, "ipv4_count": 1, "pricing_rules": { "7": 7000, "30": 25000 }, "hold_rules": { "7": 2, "30": 5 }, "setup_fee_cents": 2500, "setup_fee_rules": { "7": 2500, "30": 2500 }, "delivery_hours": 48, "setup_time_minutes": 30, "stock": null } ]}This endpoint takes no parameters.
Responses
Section titled “Responses”Returns the list of orderable offers.
{ "statusCode": 200, "message": "Offers retrieved successfully", "data": [ { "id": "507f1f77bcf86cd799439050", "name": "node-example-1", "country": "DE", "region": "eu-central", "city": "Frankfurt", "datacenter": "FRA-DC2", "cpu_model": "AMD EPYC 7402P", "cpu_cores": 24, "cpu_threads": 48, "ram_gb": 128, "disks": [ { "count": 2, "capacity_gb": 960, "type": "NVMe", "interface": "PCIe 4.0", "model": "Samsung PM983" } ], "bandwidth_mbps": 1000, "traffic_tb": 50, "traffic_unlimited": false, "ipv4_count": 1, "pricing_rules": { "7": 7000, "30": 25000 }, "hold_rules": { "7": 2, "30": 5 }, "setup_fee_cents": 2500, "setup_fee_rules": { "7": 2500, "30": 2500 }, "delivery_hours": 48, "setup_time_minutes": 30, "stock": null } ]}Reservations
Section titled “Reservations”Reservations let you commit to an offer before it is fulfilled. Charges happen at reservation time.
POST /api/v1/offers/{id}/reserve
Section titled “POST /api/v1/offers/{id}/reserve”Reserve an offer. Charges immediately for the rental and any one-time setup fee.
curl -X POST 'https://api.hoody.com/api/v1/offers/507f1f77bcf86cd799439050/reserve' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "days": 7, "max_charge_cents": 9500, "idempotency_key": "order-2025-01-21-7f3a" }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.servers.offers.reserve('507f1f77bcf86cd799439050', { days: 7, max_charge_cents: 9500, idempotency_key: 'order-2025-01-21-7f3a'});{ "statusCode": 201, "message": "Reservation created successfully", "data": { "reservation": { "id": "507f1f77bcf86cd799439060", "offer_id": "507f1f77bcf86cd799439050", "days": 7, "state": "pending", "ready_by": "2025-01-23T22:00:00.000Z", "delivery_hours_quoted": 48, "setup_time_minutes_quoted": 30, "hold_days_quoted": 2, "server_id": null, "rental_id": null, "created_at": "2025-01-21T22:00:00.000Z", "rental_cents": 7000, "setup_fee_cents": 2500, "total_paid_cents": 9500, "offer_snapshot": {} }, "replayed": false }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The offer identifier |
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
days | number | Yes | Must be one of the offer’s pricing_rules keys. |
max_charge_cents | number | No | Ceiling on the total debit (rent plus any one-time setup fee), in integer cents. Required for every paid reservation. Compute it as pricing_rules[days] plus setup_fee_rules[days] when that key is present, otherwise setup_fee_cents. A present override wins even when it is 0. |
idempotency_key | string | Yes | Caller-generated. Replaying it returns the original reservation, unpaid twice. |
pool_id | string | No | Must be a pool you own. Defaults to your default pool. |
Responses
Section titled “Responses”The reservation was created.
{ "statusCode": 201, "message": "Reservation created successfully", "data": { "reservation": { "id": "507f1f77bcf86cd799439060", "offer_id": "507f1f77bcf86cd799439050", "days": 7, "state": "pending", "ready_by": "2025-01-23T22:00:00.000Z", "delivery_hours_quoted": 48, "setup_time_minutes_quoted": 30, "hold_days_quoted": 2, "server_id": null, "rental_id": null, "created_at": "2025-01-21T22:00:00.000Z", "rental_cents": 7000, "setup_fee_cents": 2500, "total_paid_cents": 9500, "offer_snapshot": {} }, "replayed": false }}The reservation could not be created. Nothing was charged.
{ "statusCode": 409, "error": "OUT_OF_STOCK", "message": "This offer is out of stock."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
OFFER_UNAVAILABLE | Offer has been withdrawn | The listing is no longer active because it was withdrawn. | Re-read GET /api/v1/offers and present what is currently purchasable. |
OUT_OF_STOCK | Offer is sold out | Stock is exhausted. Nothing was charged. | Re-read GET /api/v1/offers; the offer may return to stock later. |
IDEMPOTENCY_INTENT_MISMATCH | Idempotency key reused for a different purchase | The supplied idempotency_key was already used for a reservation with different terms. | Use a fresh idempotency_key for a genuinely new purchase. |
SETUP_FEE_CONFIRMATION_REQUIRED | One-time setup fee must be confirmed | The machine carries a one-time setup fee, and the request omitted max_charge_cents. Nothing was charged. | Show the customer the setup_fee_cents and total_cents from data, then retry with max_charge_cents set to data.total_cents. |
CHARGE_CONFIRMATION_REQUIRED | The total must be confirmed before money moves | The request would debit an account and carried no max_charge_cents. Nothing was charged. | Read the current total, show it to the customer, and retry with max_charge_cents set to the value they confirmed. |
CHARGE_EXCEEDS_MAX | Price changed since the quote | The live total is higher than max_charge_cents. Nothing was charged. | Re-read the listing, show the user the new total, and retry with an updated max_charge_cents. |
Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}The server pricing is misconfigured and nothing was charged.
{ "statusCode": 500, "error": "PRICING_INVALID", "message": "Server pricing is misconfigured and cannot be charged. Please contact support."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PRICING_INVALID | Stored pricing is misconfigured | The server’s stored pricing did not pass validation when the charge was attempted. The request fails closed and nothing is charged. | Contact support; this server cannot be rented until its pricing is corrected. |
GET /api/v1/reservations
Section titled “GET /api/v1/reservations”List every reservation you have placed.
curl -X GET 'https://api.hoody.com/api/v1/reservations?limit=50&offset=0' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.servers.reservations.list({ limit: 50, offset: 0 });{ "statusCode": 200, "message": "Reservations retrieved successfully", "data": [ { "id": "507f1f77bcf86cd799439060", "offer_id": "507f1f77bcf86cd799439050", "days": 7, "state": "pending", "ready_by": "2025-01-23T22:00:00.000Z", "delivery_hours_quoted": 48, "setup_time_minutes_quoted": 30, "hold_days_quoted": 2, "server_id": null, "rental_id": null, "created_at": "2025-01-21T22:00:00.000Z", "rental_cents": 7000, "setup_fee_cents": 2500, "total_paid_cents": 9500, "offer_snapshot": {} } ], "total": 1, "limit": 50, "offset": 0}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
limit | query | integer | No | Maximum number of reservations to return. Default: 50. |
offset | query | integer | No | Number of reservations to skip. Default: 0. |
Responses
Section titled “Responses”Returns the caller’s reservations.
{ "statusCode": 200, "message": "Reservations retrieved successfully", "data": [ { "id": "507f1f77bcf86cd799439060", "offer_id": "507f1f77bcf86cd799439050", "days": 7, "state": "pending", "ready_by": "2025-01-23T22:00:00.000Z", "delivery_hours_quoted": 48, "setup_time_minutes_quoted": 30, "hold_days_quoted": 2, "server_id": null, "rental_id": null, "created_at": "2025-01-21T22:00:00.000Z", "rental_cents": 7000, "setup_fee_cents": 2500, "total_paid_cents": 9500, "offer_snapshot": {} } ], "total": 1, "limit": 50, "offset": 0}Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}GET /api/v1/reservations/{id}
Section titled “GET /api/v1/reservations/{id}”Fetch a single reservation by ID.
curl -X GET 'https://api.hoody.com/api/v1/reservations/507f1f77bcf86cd799439060' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.servers.reservations.get('507f1f77bcf86cd799439060');{ "statusCode": 200, "message": "Reservation retrieved successfully", "data": { "id": "507f1f77bcf86cd799439060", "offer_id": "507f1f77bcf86cd799439050", "days": 7, "state": "pending", "ready_by": "2025-01-23T22:00:00.000Z", "delivery_hours_quoted": 48, "setup_time_minutes_quoted": 30, "hold_days_quoted": 2, "server_id": null, "rental_id": null, "created_at": "2025-01-21T22:00:00.000Z", "rental_cents": 7000, "setup_fee_cents": 2500, "total_paid_cents": 9500, "offer_snapshot": {} }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The reservation identifier |
Responses
Section titled “Responses”Returns the reservation.
{ "statusCode": 200, "message": "Reservation retrieved successfully", "data": { "id": "507f1f77bcf86cd799439060", "offer_id": "507f1f77bcf86cd799439050", "days": 7, "state": "pending", "ready_by": "2025-01-23T22:00:00.000Z", "delivery_hours_quoted": 48, "setup_time_minutes_quoted": 30, "hold_days_quoted": 2, "server_id": null, "rental_id": null, "created_at": "2025-01-21T22:00:00.000Z", "rental_cents": 7000, "setup_fee_cents": 2500, "total_paid_cents": 9500, "offer_snapshot": {} }}Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}Rentals
Section titled “Rentals”Active and historical rentals. The /api/v1/servers alias routes return the same payloads as the /api/v1/rentals routes.
GET /api/v1/servers
Section titled “GET /api/v1/servers”List all servers you currently rent. Alias for GET /api/v1/rentals.
curl -X GET 'https://api.hoody.com/api/v1/servers' \ -H 'Authorization: Bearer <token>'{ "statusCode": 200, "message": "Rentals retrieved successfully", "data": [ { "id": "507f1f77bcf86cd799439320", "rental_start": "2025-01-21T22:00:00.000Z", "rental_end": "2025-01-28T22:00:00.000Z", "status": "active", "amount": "70.00", "setup_fee_cents": 2500, "total_paid_cents": 9500, "renewal_pricing_frozen": { "30": 8000 }, "remaining_days": 6, "server_id": "507f1f77bcf86cd799439014", "pool_id": "507f1f77bcf86cd799439300", "is_free_tier": false, "server": { "id": "507f1f77bcf86cd799439014", "kvm_supported": true, "kvm_nested": false, "kvm_checked_at": "2026-10-07T12:00:00.000Z" } } ]}This endpoint takes no parameters.
Responses
Section titled “Responses”Returns the caller’s rentals.
{ "statusCode": 200, "message": "Rentals retrieved successfully", "data": [ { "id": "507f1f77bcf86cd799439320", "rental_start": "2025-01-21T22:00:00.000Z", "rental_end": "2025-01-28T22:00:00.000Z", "status": "active", "amount": "70.00", "setup_fee_cents": 2500, "total_paid_cents": 9500, "renewal_pricing_frozen": { "30": 8000 }, "remaining_days": 6, "server_id": "507f1f77bcf86cd799439014", "pool_id": "507f1f77bcf86cd799439300", "is_free_tier": false, "server": { "id": "507f1f77bcf86cd799439014", "kvm_supported": true, "kvm_nested": false, "kvm_checked_at": "2026-10-07T12:00:00.000Z" } } ]}GET /api/v1/rentals
Section titled “GET /api/v1/rentals”List every rental you currently hold.
curl -X GET 'https://api.hoody.com/api/v1/rentals' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
for await (const item of client.api.servers.listIterator()) { console.log(item);}{ "statusCode": 200, "message": "Rentals retrieved successfully", "data": [ { "id": "507f1f77bcf86cd799439320", "rental_start": "2025-01-21T22:00:00.000Z", "rental_end": "2025-01-28T22:00:00.000Z", "status": "active", "amount": "70.00", "setup_fee_cents": 2500, "total_paid_cents": 9500, "renewal_pricing_frozen": { "30": 8000 }, "remaining_days": 6, "server_id": "507f1f77bcf86cd799439014", "pool_id": "507f1f77bcf86cd799439300", "is_free_tier": false, "server": { "id": "507f1f77bcf86cd799439014", "kvm_supported": true, "kvm_nested": false, "kvm_checked_at": "2026-10-07T12:00:00.000Z" } } ]}This endpoint takes no parameters.
Responses
Section titled “Responses”Returns the caller’s rentals.
{ "statusCode": 200, "message": "Rentals retrieved successfully", "data": [ { "id": "507f1f77bcf86cd799439320", "rental_start": "2025-01-21T22:00:00.000Z", "rental_end": "2025-01-28T22:00:00.000Z", "status": "active", "amount": "70.00", "setup_fee_cents": 2500, "total_paid_cents": 9500, "renewal_pricing_frozen": { "30": 8000 }, "remaining_days": 6, "server_id": "507f1f77bcf86cd799439014", "pool_id": "507f1f77bcf86cd799439300", "is_free_tier": false, "server": { "id": "507f1f77bcf86cd799439014", "kvm_supported": true, "kvm_nested": false, "kvm_checked_at": "2026-10-07T12:00:00.000Z" } } ]}GET /api/v1/servers/{id}
Section titled “GET /api/v1/servers/{id}”Get one rented server by ID. Alias for GET /api/v1/rentals/{id}.
curl -X GET 'https://api.hoody.com/api/v1/servers/507f1f77bcf86cd799439320' \ -H 'Authorization: Bearer <token>'{ "statusCode": 200, "message": "Rental details retrieved successfully", "data": { "id": "507f1f77bcf86cd799439320", "rental_start": "2025-01-21T22:00:00.000Z", "rental_end": "2025-01-28T22:00:00.000Z", "hold_days": 2, "status": "active", "amount": "70.00", "setup_fee_cents": 2500, "total_paid_cents": 9500, "renewal_pricing_frozen": { "30": 8000 }, "remaining_days": 6, "usage_days": 1, "server_id": "507f1f77bcf86cd799439014", "pool_id": "507f1f77bcf86cd799439300", "is_free_tier": false, "server": { "id": "507f1f77bcf86cd799439014", "name": "node-example-1", "country": "US", "region": "us-east", "city": "New York", "datacenter": "NYC-DC1", "model": "Intel Xeon E5-2680 v4", "is_vm": false, "specs": { "cpu": { "model": "AMD EPYC 7763", "cores": 64, "threads": 128, "score": 48500, "score_type": "passmark" }, "ram": { "capacity_gb": 256, "type": "ECC DDR5", "speed_mhz": 4800 }, "disks": { "config": [ { "count": 2, "capacity_gb": 1000, "type": "NVMe", "interface": "PCIe 4.0" }, { "count": 6, "capacity_gb": 15000, "type": "NVMe", "interface": "PCIe 4.0" } ], "total_gb": 92000, "summary": "2x1TB NVMe + 6x15TB NVMe" }, "network": { "bandwidth_mbps": 10000, "bandwidth_formatted": "10 Gbps", "traffic_tb": 100, "traffic_unlimited": false }, "additional": { "ipv4_count": 5, "ipv6_enabled": true } }, "kvm_supported": true, "kvm_nested": false, "kvm_checked_at": "2026-10-07T12:00:00.000Z" }, "transaction": { "id": "507f1f77bcf86cd799439321", "amount": 70, "currency": "USD", "created_at": "2025-01-21T22:00:00.000Z" } }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The rental identifier |
Responses
Section titled “Responses”Returns the rental.
{ "statusCode": 200, "message": "Rental details retrieved successfully", "data": { "id": "507f1f77bcf86cd799439320", "rental_start": "2025-01-21T22:00:00.000Z", "rental_end": "2025-01-28T22:00:00.000Z", "hold_days": 2, "status": "active", "amount": "70.00", "setup_fee_cents": 2500, "total_paid_cents": 9500, "renewal_pricing_frozen": { "30": 8000 }, "remaining_days": 6, "usage_days": 1, "server_id": "507f1f77bcf86cd799439014", "pool_id": "507f1f77bcf86cd799439300", "is_free_tier": false, "server": { "id": "507f1f77bcf86cd799439014", "name": "node-example-1", "country": "US", "region": "us-east", "city": "New York", "datacenter": "NYC-DC1", "model": "Intel Xeon E5-2680 v4", "is_vm": false, "specs": { "cpu": { "model": "AMD EPYC 7763", "cores": 64, "threads": 128, "score": 48500, "score_type": "passmark" }, "ram": { "capacity_gb": 256, "type": "ECC DDR5", "speed_mhz": 4800 }, "disks": { "config": [ { "count": 2, "capacity_gb": 1000, "type": "NVMe", "interface": "PCIe 4.0" }, { "count": 6, "capacity_gb": 15000, "type": "NVMe", "interface": "PCIe 4.0" } ], "total_gb": 92000, "summary": "2x1TB NVMe + 6x15TB NVMe" }, "network": { "bandwidth_mbps": 10000, "bandwidth_formatted": "10 Gbps", "traffic_tb": 100, "traffic_unlimited": false }, "additional": { "ipv4_count": 5, "ipv6_enabled": true } }, "kvm_supported": true, "kvm_nested": false, "kvm_checked_at": "2026-10-07T12:00:00.000Z" }, "transaction": { "id": "507f1f77bcf86cd799439321", "amount": 70, "currency": "USD", "created_at": "2025-01-21T22:00:00.000Z" } }}Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}GET /api/v1/rentals/{id}
Section titled “GET /api/v1/rentals/{id}”Get one rental by ID.
curl -X GET 'https://api.hoody.com/api/v1/rentals/507f1f77bcf86cd799439320' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.servers.get('507f1f77bcf86cd799439320');{ "statusCode": 200, "message": "Rental retrieved successfully", "data": { "id": "507f1f77bcf86cd799439320", "rental_start": "2025-01-21T22:00:00.000Z", "rental_end": "2025-01-28T22:00:00.000Z", "hold_days": 2, "status": "active", "amount": "70.00", "setup_fee_cents": 2500, "total_paid_cents": 9500, "renewal_pricing_frozen": { "30": 8000 }, "remaining_days": 6, "usage_days": 1, "server_id": "507f1f77bcf86cd799439014", "pool_id": "507f1f77bcf86cd799439300", "is_free_tier": false, "server": { "id": "507f1f77bcf86cd799439014", "name": "node-example-1", "country": "US", "region": "us-east", "city": "New York", "datacenter": "NYC-DC1", "model": "Intel Xeon E5-2680 v4", "is_vm": false, "specs": { "cpu": { "model": "AMD EPYC 7763", "cores": 64, "threads": 128, "score": 48500, "score_type": "passmark" }, "ram": { "capacity_gb": 256, "type": "ECC DDR5", "speed_mhz": 4800 }, "disks": { "config": [ { "count": 2, "capacity_gb": 1000, "type": "NVMe", "interface": "PCIe 4.0" }, { "count": 6, "capacity_gb": 15000, "type": "NVMe", "interface": "PCIe 4.0" } ], "total_gb": 92000, "summary": "2x1TB NVMe + 6x15TB NVMe" }, "network": { "bandwidth_mbps": 10000, "bandwidth_formatted": "10 Gbps", "traffic_tb": 100, "traffic_unlimited": false }, "additional": { "ipv4_count": 5, "ipv6_enabled": true } }, "kvm_supported": true, "kvm_nested": false, "kvm_checked_at": "2026-10-07T12:00:00.000Z" }, "transaction": { "id": "507f1f77bcf86cd799439321", "amount": 70, "currency": "USD", "created_at": "2025-01-21T22:00:00.000Z" } }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The rental identifier |
Responses
Section titled “Responses”Returns the rental.
{ "statusCode": 200, "message": "Rental retrieved successfully", "data": { "id": "507f1f77bcf86cd799439320", "rental_start": "2025-01-21T22:00:00.000Z", "rental_end": "2025-01-28T22:00:00.000Z", "hold_days": 2, "status": "active", "amount": "70.00", "setup_fee_cents": 2500, "total_paid_cents": 9500, "renewal_pricing_frozen": { "30": 8000 }, "remaining_days": 6, "usage_days": 1, "server_id": "507f1f77bcf86cd799439014", "pool_id": "507f1f77bcf86cd799439300", "is_free_tier": false, "server": { "id": "507f1f77bcf86cd799439014", "name": "node-example-1", "country": "US", "region": "us-east", "city": "New York", "datacenter": "NYC-DC1", "model": "Intel Xeon E5-2680 v4", "is_vm": false, "specs": { "cpu": { "model": "AMD EPYC 7763", "cores": 64, "threads": 128, "score": 48500, "score_type": "passmark" }, "ram": { "capacity_gb": 256, "type": "ECC DDR5", "speed_mhz": 4800 }, "disks": { "config": [ { "count": 2, "capacity_gb": 1000, "type": "NVMe", "interface": "PCIe 4.0" }, { "count": 6, "capacity_gb": 15000, "type": "NVMe", "interface": "PCIe 4.0" } ], "total_gb": 92000, "summary": "2x1TB NVMe + 6x15TB NVMe" }, "network": { "bandwidth_mbps": 10000, "bandwidth_formatted": "10 Gbps", "traffic_tb": 100, "traffic_unlimited": false }, "additional": { "ipv4_count": 5, "ipv6_enabled": true } }, "kvm_supported": true, "kvm_nested": false, "kvm_checked_at": "2026-10-07T12:00:00.000Z" }, "transaction": { "id": "507f1f77bcf86cd799439321", "amount": 70, "currency": "USD", "created_at": "2025-01-21T22:00:00.000Z" } }}Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}POST /api/v1/servers/{id}/rent
Section titled “POST /api/v1/servers/{id}/rent”Rent an available server. Some servers carry a one-time setup fee charged only on this first payment (never on extension); when they do, the request must confirm the total via max_charge_cents.
curl -X POST 'https://api.hoody.com/api/v1/servers/507f1f77bcf86cd799439014/rent' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "rental_days": 7, "max_charge_cents": 9500 }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.servers.rent('507f1f77bcf86cd799439014', { rental_days: 7, max_charge_cents: 9500});{ "statusCode": 201, "message": "Server rented successfully", "data": { "rental": { "id": "507f1f77bcf86cd799439320", "server_id": "507f1f77bcf86cd799439014", "rental_start": "2025-01-21T22:00:00.000Z", "rental_end": "2025-01-28T22:00:00.000Z", "hold_days": 2, "actual_usage_days": 0, "status": "active", "setup_fee_cents": 2500, "total_paid_cents": 12500, "renewal_pricing_frozen": { "30": 8000 } }, "transaction": { "id": "507f1f77bcf86cd799439321", "amount": 70, "currency": "USD" } }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The server identifier |
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
pool_id | string | No | Pool to assign the rental to. Must match ^[0-9a-f]{24}$. |
rental_days | integer | Yes | Number of days to rent. Must match a server-priced duration, between 1 and 3650. |
max_charge_cents | integer | No | Ceiling on the total debit (rental price plus any one-time setup fee), in integer cents. Required for every paid rental. Read it from pricing.price_tiers[rental_days].total_first_payment. |
Responses
Section titled “Responses”The rental was created.
{ "statusCode": 201, "message": "Server rented successfully", "data": { "rental": { "id": "507f1f77bcf86cd799439320", "server_id": "507f1f77bcf86cd799439014", "rental_start": "2025-01-21T22:00:00.000Z", "rental_end": "2025-01-28T22:00:00.000Z", "hold_days": 2, "actual_usage_days": 0, "status": "active", "setup_fee_cents": 2500, "total_paid_cents": 12500, "renewal_pricing_frozen": { "30": 8000 } }, "transaction": { "id": "507f1f77bcf86cd799439321", "amount": 70, "currency": "USD" } }}The request could not be processed.
{ "statusCode": 400, "error": "INSUFFICIENT_BALANCE", "message": "Insufficient balance for rental", "data": { "rental_cents": 20000, "setup_fee_cents": 2500, "required_cents": 22500, "available_cents": 18000 }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSUFFICIENT_BALANCE | Wallet balance too low | The wallet balance is below the total required. data carries rental_cents, setup_fee_cents, required_cents, and available_cents. | Top up the wallet to at least required_cents, then retry. |
INVALID_DURATION | Rental duration not offered | rental_days must match a duration the server actually prices. | Pick one of pricing.available_durations and retry. |
NO_PRICING | Server has no pricing configured | The server has no pricing row, or none covering the requested duration. | Choose a different server, or contact support. |
The rental could not be created.
{ "statusCode": 409, "error": "SERVER_UNAVAILABLE", "message": "Server not available for rental"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
SERVER_UNAVAILABLE | Server not available for rental | The server is already rented, not ready, or not offered for rental. | Re-read GET /api/v1/servers/available and pick a currently available server. |
SETUP_FEE_CONFIRMATION_REQUIRED | One-time setup fee must be confirmed | The machine carries a one-time setup fee, and the request omitted max_charge_cents. Nothing was charged. | Show the customer the setup_fee_cents and total_cents from data, then retry with max_charge_cents set to data.total_cents. |
CHARGE_CONFIRMATION_REQUIRED | The total must be confirmed before money moves | The request would debit an account and carried no max_charge_cents. Nothing was charged. | Read the current total, show it to the customer, and retry with max_charge_cents set to the value they confirmed. |
CHARGE_EXCEEDS_MAX | Price changed since the quote | The live total is higher than max_charge_cents. Nothing was charged. | Re-read the listing, show the user the new total, and retry with an updated max_charge_cents. |
Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}The server pricing is misconfigured and nothing was charged.
{ "statusCode": 500, "error": "PRICING_INVALID", "message": "Server pricing is misconfigured and cannot be charged. Please contact support."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PRICING_INVALID | Stored pricing is misconfigured | The server’s stored pricing did not pass validation when the charge was attempted. The request fails closed and nothing is charged. | Contact support; this server cannot be rented until its pricing is corrected. |
POST /api/v1/rentals/{id}/extend
Section titled “POST /api/v1/rentals/{id}/extend”Extend an existing rental for additional days. Sends expected_rental_end to make replay safe.
curl -X POST 'https://api.hoody.com/api/v1/rentals/507f1f77bcf86cd799439320/extend' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "expected_rental_end": "2025-01-28T22:00:00.000Z", "additional_days": 7, "max_charge_cents": 7000 }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.servers.extend('507f1f77bcf86cd799439320', { expected_rental_end: '2025-01-28T22:00:00.000Z', additional_days: 7, max_charge_cents: 7000});{ "statusCode": 200, "message": "Rental extended successfully", "data": { "rental": { "id": "507f1f77bcf86cd799439320", "rental_end": "2025-02-04T22:00:00.000Z", "status": "active", "amount": "140.00", "setup_fee_cents": 2500, "total_paid_cents": 9500, "renewal_pricing_frozen": { "30": 8000 }, "remaining_days": 13 }, "transaction": { "id": "507f1f77bcf86cd799439322", "amount": 70, "currency": "USD" } }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The rental identifier |
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
expected_rental_end | string | Yes | The rental’s current rental_end, exactly as the API returned it. The extension is applied only if it still matches. |
additional_days | integer | Yes | Number of additional days to extend the rental. Between 1 and 3650, must match server pricing durations. |
max_charge_cents | integer | No | The total you confirmed, in whole cents. Required when the rental has no frozen renewal price. |
Responses
Section titled “Responses”The rental was extended.
{ "statusCode": 200, "message": "Rental extended successfully", "data": { "rental": { "id": "507f1f77bcf86cd799439320", "rental_end": "2025-02-04T22:00:00.000Z", "status": "active", "amount": "140.00", "setup_fee_cents": 2500, "total_paid_cents": 9500, "renewal_pricing_frozen": { "30": 8000 }, "remaining_days": 13 }, "transaction": { "id": "507f1f77bcf86cd799439322", "amount": 70, "currency": "USD" } }}The extension was not applied. Nothing was charged.
{ "statusCode": 409, "error": "EXTENSION_ALREADY_APPLIED", "message": "This rental has already been extended. Re-read it and confirm the new total before extending again.", "data": { "rental_id": "000000000000000000000001", "rental_end": "2026-09-01T00:00:00.000Z", "expected_rental_end": "2026-08-01T00:00:00.000Z" }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
EXTENSION_ALREADY_APPLIED | That extension has already been applied | The rental’s end date no longer matches expected_rental_end. Nothing was charged. | Re-read the rental. If its end date already reflects the extension you wanted, you are done. Otherwise retry with expected_rental_end set to the value you just read. |
CHARGE_CONFIRMATION_REQUIRED | The total must be confirmed before money moves | The request would debit an account and carried no max_charge_cents. Nothing was charged. | Read the current total, show it to the customer, and retry with max_charge_cents set to the value they confirmed. |
CHARGE_EXCEEDS_MAX | Price changed since the quote | The live total is higher than max_charge_cents. Nothing was charged. | Re-read the listing, show the user the new total, and retry with an updated max_charge_cents. |
Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}Live runtime
Section titled “Live runtime”Short-TTL live CPU, memory, disk, and load metrics for a rented server or subserver. Polled frequently; first-party JWT only.
GET /api/v1/servers/{id}/runtime
Section titled “GET /api/v1/servers/{id}/runtime”Live runtime info for a rented server or subserver. Alias for GET /api/v1/rentals/{id}/runtime.
curl -X GET 'https://api.hoody.com/api/v1/servers/507f1f77bcf86cd799439014/runtime' \ -H 'Authorization: Bearer <token>'{ "statusCode": 200, "message": "Runtime info retrieved successfully", "data": { "scope": "server", "ts": "2025-01-22T10:00:00.000Z", "cache_age_ms": 1500, "stale": false, "uptime_s": 86400, "cores": 64, "load": { "1": 0.42, "5": 0.38, "15": 0.31 }, "cpu": { "busy_pct": 12.4 }, "mem": { "used_bytes": 85899345920, "total_bytes": 274877906944, "pct": 31.2 }, "swap": { "used_bytes": 0, "total_bytes": 8589934592 }, "storage": { "used_bytes": 4500000000000, "total_bytes": 92000000000000, "pct": 4.9 } }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The rental or server identifier |
Responses
Section titled “Responses”Returns live runtime counters.
{ "statusCode": 200, "message": "Runtime info retrieved successfully", "data": { "scope": "server", "ts": "2025-01-22T10:00:00.000Z", "cache_age_ms": 1500, "stale": false, "uptime_s": 86400, "cores": 64, "load": { "1": 0.42, "5": 0.38, "15": 0.31 }, "cpu": { "busy_pct": 12.4 }, "mem": { "used_bytes": 85899345920, "total_bytes": 274877906944, "pct": 31.2 }, "swap": { "used_bytes": 0, "total_bytes": 8589934592 }, "storage": { "used_bytes": 4500000000000, "total_bytes": 92000000000000, "pct": 4.9 } }}The rental or server identifier is not a 24-character hexadecimal string.
{ "statusCode": 400, "error": "Bad Request", "message": "Invalid ID format"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_ID_FORMAT | Invalid ID format | The provided ID must be a 24-character hexadecimal string. | Ensure the ID is exactly 24 characters long and contains only hexadecimal characters (0-9, a-f). |
Authentication failed.
{ "statusCode": 401, "error": "Unauthorized", "message": "Authentication token required"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TOKEN | Authentication token missing | No authentication token was provided in the request. | Include a valid JWT token in the Authorization header as Bearer <token>. |
INVALID_TOKEN | Invalid authentication token | The provided authentication token is malformed or invalid. | Obtain a new token by logging in again or using a valid auth token. |
AUTH_TOKEN_LINEAGE_REVOKED | Parent auth token no longer valid | The auth token was created by another auth token, and that token (or one above it) has been revoked, disabled or has expired, so this one no longer authenticates | Create a new auth token from a valid one, or sign in again |
EMAIL_NOT_VERIFIED | Email not verified | Returned by the login endpoint when the password is correct but the account’s email address has not been verified yet. It is only returned after the password has been checked. The response carries data.email so the client can offer a ‘resend verification email’ action without asking again. | Complete email verification by clicking the link sent to your inbox, or call /auth/resend-verification to receive a new link, or complete a password reset which also implicitly verifies the email. |
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired | Generate a new code from your authenticator app and try again |
The caller does not have permission to read runtime for this resource.
{ "statusCode": 403, "error": "Forbidden", "message": "Insufficient permissions"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSUFFICIENT_PERMISSIONS | Insufficient permissions | You do not have the required permissions to perform this action. | Contact the resource owner or administrator to request access. |
The rental or server does not exist.
{ "statusCode": 404, "error": "Not Found", "message": "Resource not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
RESOURCE_NOT_FOUND | Resource not found | The requested resource does not exist or has been deleted. | Verify the resource ID and ensure it exists. |
Host-level runtime is not exposed for a shared or system server.
{ "statusCode": 409, "error": "Conflict", "message": "Host runtime not available for a shared server"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
RUNTIME_HOST_SHARED | Host runtime not available for a shared server | Host-level runtime info is not exposed for a physical server that hosts subservers or is a system server. | Query runtime for the specific subserver you rent instead of the whole physical host. |
Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}You have exceeded the rate limit for this endpoint.
{ "statusCode": 429, "error": "Too Many Requests", "message": "Rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
RATE_LIMIT_EXCEEDED | Rate limit exceeded | You have exceeded the rate limit for this endpoint. | Wait before making additional requests, or upgrade your plan for higher limits. |
Live runtime info could not be collected.
{ "statusCode": 503, "error": "Service Unavailable", "message": "Runtime info temporarily unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
RUNTIME_UNAVAILABLE | Runtime info temporarily unavailable | Live runtime info could not be collected for this server and no recent snapshot is available. | Retry shortly. If it persists, the host may be unreachable or under maintenance. |
GET /api/v1/rentals/{id}/runtime
Section titled “GET /api/v1/rentals/{id}/runtime”Live runtime info for a rented server or subserver.
curl -X GET 'https://api.hoody.com/api/v1/rentals/507f1f77bcf86cd799439014/runtime' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.servers.getStats('507f1f77bcf86cd799439014');{ "statusCode": 200, "message": "Runtime info retrieved successfully", "data": { "scope": "server", "ts": "2025-01-22T10:00:00.000Z", "cache_age_ms": 1500, "stale": false, "uptime_s": 86400, "cores": 64, "load": { "1": 0.42, "5": 0.38, "15": 0.31 }, "cpu": { "busy_pct": 12.4 }, "mem": { "used_bytes": 85899345920, "total_bytes": 274877906944, "pct": 31.2 }, "swap": { "used_bytes": 0, "total_bytes": 8589934592 }, "storage": { "used_bytes": 4500000000000, "total_bytes": 92000000000000, "pct": 4.9 } }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The rental identifier |
Responses
Section titled “Responses”Returns live runtime counters.
{ "statusCode": 200, "message": "Runtime info retrieved successfully", "data": { "scope": "server", "ts": "2025-01-22T10:00:00.000Z", "cache_age_ms": 1500, "stale": false, "uptime_s": 86400, "cores": 64, "load": { "1": 0.42, "5": 0.38, "15": 0.31 }, "cpu": { "busy_pct": 12.4 }, "mem": { "used_bytes": 85899345920, "total_bytes": 274877906944, "pct": 31.2 }, "swap": { "used_bytes": 0, "total_bytes": 8589934592 }, "storage": { "used_bytes": 4500000000000, "total_bytes": 92000000000000, "pct": 4.9 } }}The rental identifier is not a 24-character hexadecimal string.
{ "statusCode": 400, "error": "Bad Request", "message": "Invalid ID format"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_ID_FORMAT | Invalid ID format | The provided ID must be a 24-character hexadecimal string. | Ensure the ID is exactly 24 characters long and contains only hexadecimal characters (0-9, a-f). |
Authentication failed.
{ "statusCode": 401, "error": "Unauthorized", "message": "Authentication token required"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TOKEN | Authentication token missing | No authentication token was provided in the request. | Include a valid JWT token in the Authorization header as Bearer <token>. |
INVALID_TOKEN | Invalid authentication token | The provided authentication token is malformed or invalid. | Obtain a new token by logging in again or using a valid auth token. |
AUTH_TOKEN_LINEAGE_REVOKED | Parent auth token no longer valid | The auth token was created by another auth token, and that token (or one above it) has been revoked, disabled or has expired, so this one no longer authenticates | Create a new auth token from a valid one, or sign in again |
EMAIL_NOT_VERIFIED | Email not verified | Returned by the login endpoint when the password is correct but the account’s email address has not been verified yet. It is only returned after the password has been checked. The response carries data.email so the client can offer a ‘resend verification email’ action without asking again. | Complete email verification by clicking the link sent to your inbox, or call /auth/resend-verification to receive a new link, or complete a password reset which also implicitly verifies the email. |
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired | Generate a new code from your authenticator app and try again |
The caller does not have permission to read runtime for this rental.
{ "statusCode": 403, "error": "Forbidden", "message": "Insufficient permissions"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSUFFICIENT_PERMISSIONS | Insufficient permissions | You do not have the required permissions to perform this action. | Contact the resource owner or administrator to request access. |
The rental does not exist.
{ "statusCode": 404, "error": "Not Found", "message": "Resource not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
RESOURCE_NOT_FOUND | Resource not found | The requested resource does not exist or has been deleted. | Verify the resource ID and ensure it exists. |
Host-level runtime is not exposed for a shared or system server.
{ "statusCode": 409, "error": "Conflict", "message": "Host runtime not available for a shared server"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
RUNTIME_HOST_SHARED | Host runtime not available for a shared server | Host-level runtime info is not exposed for a physical server that hosts subservers or is a system server. | Query runtime for the specific subserver you rent instead of the whole physical host. |
Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}You have exceeded the rate limit for this endpoint.
{ "statusCode": 429, "error": "Too Many Requests", "message": "Rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
RATE_LIMIT_EXCEEDED | Rate limit exceeded | You have exceeded the rate limit for this endpoint. | Wait before making additional requests, or upgrade your plan for higher limits. |
Live runtime info could not be collected.
{ "statusCode": 503, "error": "Service Unavailable", "message": "Runtime info temporarily unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
RUNTIME_UNAVAILABLE | Runtime info temporarily unavailable | Live runtime info could not be collected for this server and no recent snapshot is available. | Retry shortly. If it persists, the host may be unreachable or under maintenance. |
Pools group rented servers so multiple users can share them with a role.
GET /api/v1/pools
Section titled “GET /api/v1/pools”List every pool you own or are a member of.
curl -X GET 'https://api.hoody.com/api/v1/pools' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
for await (const item of client.api.pools.listIterator()) { console.log(item);}{ "statusCode": 200, "message": "Pools retrieved successfully", "data": [ { "id": "507f1f77bcf86cd799439300", "name": "Development Team", "description": "Pool for development team resources", "is_default": false, "settings": {}, "created_at": "2025-01-15T10:00:00.000Z", "updated_at": "2025-01-20T14:30:00.000Z", "owner_id": "507f1f77bcf86cd799439011", "user_role": "owner", "member_count": 5, "server_count": 3 } ]}This endpoint takes no parameters.
Responses
Section titled “Responses”Returns pools owned by or shared with the caller.
{ "statusCode": 200, "message": "Pools retrieved successfully", "data": [ { "id": "507f1f77bcf86cd799439300", "name": "Development Team", "description": "Pool for development team resources", "is_default": false, "settings": {}, "created_at": "2025-01-15T10:00:00.000Z", "updated_at": "2025-01-20T14:30:00.000Z", "owner_id": "507f1f77bcf86cd799439011", "user_role": "owner", "member_count": 5, "server_count": 3 } ]}POST /api/v1/pools
Section titled “POST /api/v1/pools”Create a new pool.
curl -X POST 'https://api.hoody.com/api/v1/pools' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "name": "Production Team", "description": "Pool for production environment management", "settings": { "auto_approve": true, "max_servers": 20 } }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.pools.create({ name: 'Production Team', description: 'Pool for production environment management', settings: { auto_approve: true, max_servers: 20 }});{ "statusCode": 201, "message": "Pool created successfully", "data": { "id": "507f1f77bcf86cd799439301", "name": "Production Team", "description": "Pool for production environment management", "owner_id": "507f1f77bcf86cd799439011", "is_default": false, "settings": {}, "created_at": "2025-01-21T22:00:00.000Z" }}This endpoint takes no parameters.
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Pool name. Up to 100 characters. |
description | string | No | Pool description. Up to 500 characters. |
settings | object | No | Free-form settings map. |
Responses
Section titled “Responses”The pool was created.
{ "statusCode": 201, "message": "Pool created successfully", "data": { "id": "507f1f77bcf86cd799439301", "name": "Production Team", "description": "Pool for production environment management", "owner_id": "507f1f77bcf86cd799439011", "is_default": false, "settings": {}, "created_at": "2025-01-21T22:00:00.000Z" }}Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}GET /api/v1/pools/{id}
Section titled “GET /api/v1/pools/{id}”Get detailed information about a single pool.
curl -X GET 'https://api.hoody.com/api/v1/pools/507f1f77bcf86cd799439300' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.pools.get('507f1f77bcf86cd799439300');{ "statusCode": 200, "message": "Pool retrieved successfully", "data": { "id": "507f1f77bcf86cd799439300", "name": "Development Team", "description": "Pool for development team resources", "is_default": false, "settings": {}, "created_at": "2025-01-15T10:00:00.000Z", "updated_at": "2025-01-20T14:30:00.000Z", "owner_id": "507f1f77bcf86cd799439011", "members": [ { "id": "507f1f77bcf86cd799439310", "role": "admin", "is_authorized": true, "joined_at": "2025-01-16T11:00:00.000Z", "user": { "id": "507f1f77bcf86cd799439012", "username": "jane_admin", "alias": "Jane Smith" } } ], "servers": [ { "id": "507f1f77bcf86cd799439014", "name": "node-example-1", "rental_status": "active", "is_ready": true } ] }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The pool identifier |
Responses
Section titled “Responses”Returns the pool with its members and servers.
{ "statusCode": 200, "message": "Pool retrieved successfully", "data": { "id": "507f1f77bcf86cd799439300", "name": "Development Team", "description": "Pool for development team resources", "is_default": false, "settings": {}, "created_at": "2025-01-15T10:00:00.000Z", "updated_at": "2025-01-20T14:30:00.000Z", "owner_id": "507f1f77bcf86cd799439011", "members": [ { "id": "507f1f77bcf86cd799439310", "role": "admin", "is_authorized": true, "joined_at": "2025-01-16T11:00:00.000Z", "user": { "id": "507f1f77bcf86cd799439012", "username": "jane_admin", "alias": "Jane Smith" } } ], "servers": [ { "id": "507f1f77bcf86cd799439014", "name": "node-example-1", "rental_status": "active", "is_ready": true } ] }}Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}PUT /api/v1/pools/{id}
Section titled “PUT /api/v1/pools/{id}”Update pool details. Owner only. PATCH is also accepted on this endpoint with identical behaviour.
curl -X PUT 'https://api.hoody.com/api/v1/pools/507f1f77bcf86cd799439300' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "description": "Pool for staging and development workloads", "settings": { "auto_approve": false, "max_servers": 12 } }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.pools.update('507f1f77bcf86cd799439300', { description: 'Pool for staging and development workloads', settings: { auto_approve: false, max_servers: 12 }});{ "statusCode": 200, "message": "Pool updated successfully", "data": { "success": true }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The pool identifier |
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
description | string | No | Updated description, up to 500 characters. |
settings | object | No | Replacement settings map. |
Responses
Section titled “Responses”The pool was updated.
{ "statusCode": 200, "message": "Pool updated successfully", "data": { "success": true }}Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}DELETE /api/v1/pools/{id}
Section titled “DELETE /api/v1/pools/{id}”Delete a pool. Owner only. The default pool cannot be deleted.
curl -X DELETE 'https://api.hoody.com/api/v1/pools/507f1f77bcf86cd799439301' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.pools.delete('507f1f77bcf86cd799439301');{ "statusCode": 200, "message": "Pool deleted successfully", "data": { "success": true }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The pool identifier |
Responses
Section titled “Responses”The pool was deleted.
{ "statusCode": 200, "message": "Pool deleted successfully", "data": { "success": true }}Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}Pool members
Section titled “Pool members”Invite, role-change, and remove members within a pool. Member-management endpoints require the caller to be at least an admin of the pool.
POST /api/v1/pools/{id}/members
Section titled “POST /api/v1/pools/{id}/members”Invite a user to join the pool. Admin or owner only.
curl -X POST 'https://api.hoody.com/api/v1/pools/507f1f77bcf86cd799439300/members' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "username": "jane_admin", "role": "admin" }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.pools.members.invite('507f1f77bcf86cd799439300', { username: 'jane_admin', role: 'admin'});{ "statusCode": 201, "message": "Member invited successfully", "data": { "id": "507f1f77bcf86cd799439311", "role": "user", "is_authorized": false, "invited_at": "2025-01-21T22:15:00.000Z" }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The pool identifier |
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
username | string | Yes | Username of the user to invite. Must match ^[a-zA-Z0-9_-]+$, 1 to 100 characters. |
role | string | Yes | Role to grant on acceptance. One of admin, user. |
Responses
Section titled “Responses”The invitation was sent.
{ "statusCode": 201, "message": "Member invited successfully", "data": { "id": "507f1f77bcf86cd799439311", "role": "user", "is_authorized": false, "invited_at": "2025-01-21T22:15:00.000Z" }}Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}PUT /api/v1/pools/{id}/members/{userId}
Section titled “PUT /api/v1/pools/{id}/members/{userId}”Change a member’s role within the pool. Owner only. PATCH is also accepted on this endpoint with identical behaviour.
curl -X PUT 'https://api.hoody.com/api/v1/pools/507f1f77bcf86cd799439300/members/507f1f77bcf86cd799439012' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "role": "admin" }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.pools.members.setRole('507f1f77bcf86cd799439300', '507f1f77bcf86cd799439012', { role: 'admin'});{ "statusCode": 200, "message": "Member role updated successfully", "data": { "success": true }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The pool identifier |
userId | path | string | Yes | The member’s user identifier |
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
role | string | Yes | New role. One of admin, user. |
Responses
Section titled “Responses”The member’s role was updated.
{ "statusCode": 200, "message": "Member role updated successfully", "data": { "success": true }}Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}DELETE /api/v1/pools/{id}/members/{userId}
Section titled “DELETE /api/v1/pools/{id}/members/{userId}”Remove a member from the pool. Admin or owner only.
curl -X DELETE 'https://api.hoody.com/api/v1/pools/507f1f77bcf86cd799439300/members/507f1f77bcf86cd799439012' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.pools.members.remove('507f1f77bcf86cd799439300', '507f1f77bcf86cd799439012');{ "statusCode": 200, "message": "Member removed successfully", "data": { "success": true }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The pool identifier |
userId | path | string | Yes | The member’s user identifier |
Responses
Section titled “Responses”The member was removed.
{ "statusCode": 200, "message": "Member removed successfully", "data": { "success": true }}Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}Pool invitations
Section titled “Pool invitations”Manage your pending pool invitations, that is, invitations another user sent you that you have not yet accepted or rejected.
GET /api/v1/pools/invitations/pending
Section titled “GET /api/v1/pools/invitations/pending”List every pool invitation currently pending for the authenticated caller.
curl -X GET 'https://api.hoody.com/api/v1/pools/invitations/pending' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.pools.invitations.list();{ "statusCode": 200, "message": "Pending invitations retrieved successfully", "data": [ { "id": "507f1f77bcf86cd799439320", "pool_id": "507f1f77bcf86cd799439300", "pool_name": "Development Team", "pool_description": "Pool for development team resources", "role": "admin", "invited_by": { "id": "507f1f77bcf86cd799439011", "username": "owner_jane", "alias": "Jane Smith" }, "invited_at": "2025-01-21T22:00:00.000Z" } ]}This endpoint takes no parameters.
Responses
Section titled “Responses”Returns pending invitations.
{ "statusCode": 200, "message": "Pending invitations retrieved successfully", "data": [ { "id": "507f1f77bcf86cd799439320", "pool_id": "507f1f77bcf86cd799439300", "pool_name": "Development Team", "pool_description": "Pool for development team resources", "role": "admin", "invited_by": { "id": "507f1f77bcf86cd799439011", "username": "owner_jane", "alias": "Jane Smith" }, "invited_at": "2025-01-21T22:00:00.000Z" } ]}{ "statusCode": 401, "error": "MISSING_TOKEN", "message": "Authentication token required"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TOKEN | Authentication token missing | No authentication token was provided in the request. | Include a valid JWT token in the Authorization header as Bearer <token>. |
INVALID_TOKEN | Invalid authentication token | The provided authentication token is malformed or invalid. | Obtain a new token by logging in again or using a valid auth token. |
TOKEN_EXPIRED | Authentication token expired | The provided authentication token has expired. | Obtain a new token by logging in again or refreshing your session. |
AUTH_TOKEN_LINEAGE_REVOKED | Parent auth token no longer valid | The auth token was created by another auth token, and that token (or one above it) has been revoked, disabled or has expired, so this one no longer authenticates. | Create a new auth token from a valid one, or sign in again. |
INVALID_CREDENTIALS | Invalid credentials | The provided username/email or password is incorrect. | Verify your credentials and try again, or use the password reset feature. |
EMAIL_NOT_VERIFIED | Email not verified | Returned by the login endpoint when the password is correct but the account’s email address has not been verified yet. It is only returned after the password has been checked. The response carries data.email so the client can offer a ‘resend verification email’ action without asking again. | Complete email verification by clicking the link sent to your inbox, or call /auth/resend-verification to receive a new link, or complete a password reset which also implicitly verifies the email. |
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired. | Generate a new code from your authenticator app and try again. |
{ "statusCode": 403, "error": "INSUFFICIENT_PERMISSIONS", "message": "Insufficient permissions"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSUFFICIENT_PERMISSIONS | Insufficient permissions | You do not have the required permissions to perform this action. | Ask the resource owner to grant you access. |
ACCOUNT_BANNED | Account banned | Your account has been banned and cannot access this resource. | Contact support for information about your account status. |
POST /api/v1/pools/{id}/accept
Section titled “POST /api/v1/pools/{id}/accept”Accept a pending pool invitation.
curl -X POST 'https://api.hoody.com/api/v1/pools/507f1f77bcf86cd799439320/accept' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.pools.invitations.accept('507f1f77bcf86cd799439320');{ "statusCode": 200, "message": "Invitation accepted successfully", "data": { "success": true }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The invitation identifier |
Responses
Section titled “Responses”The invitation was accepted.
{ "statusCode": 200, "message": "Invitation accepted successfully", "data": { "success": true }}Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}POST /api/v1/pools/{id}/reject
Section titled “POST /api/v1/pools/{id}/reject”Reject a pending pool invitation.
curl -X POST 'https://api.hoody.com/api/v1/pools/507f1f77bcf86cd799439320/reject' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.pools.invitations.reject('507f1f77bcf86cd799439320');{ "statusCode": 200, "message": "Invitation rejected successfully", "data": { "success": true }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The invitation identifier |
Responses
Section titled “Responses”The invitation was rejected.
{ "statusCode": 200, "message": "Invitation rejected successfully", "data": { "success": true }}Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}Paid subserver plans
Section titled “Paid subserver plans”The monthly plans you can buy as a paid subserver. The list mixes public plans with offers only signed-in users see, and each field’s text falls back to English when no translation exists. An auth token needs the financial.server_rental.view_marketplace permission.
GET /api/v1/subserver-plans
Section titled “GET /api/v1/subserver-plans”List every paid subserver plan you can buy.
curl -X GET 'https://api.hoody.com/api/v1/subserver-plans?locale=en' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.servers.plans.list({ locale: 'en' });{ "statusCode": 200, "message": "OK", "data": { "requested_locale": "en", "plans": [ { "id": "507f1f77bcf86cd799439400", "family_id": "plan_std_8_32_500", "type": "standard", "sort_order": 10, "price_cents_per_month": 5000, "hold_days": 7, "resources": { "cpu_cores": 8, "ram_gb": 32, "disk_limit_gb": 500, "disk_io_read_mib": 1024, "disk_io_write_mib": 512, "network_limit_mbps": 1000, "max_containers": 50, "snapshots_per_container": 10, "ai_multiplier": 1 }, "text": { "title": "Standard 8", "description": "8 vCPU, 32 GB RAM, 500 GB NVMe.", "type_label": "Standard", "features": ["Dedicated resources", "Full root access", "Snapshots included"] }, "text_locales": { "title": "en", "description": "en", "type_label": "en", "features": "en" } } ] }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
locale | query | string | No | Language tag such as en, fr or pt-BR. Overrides Accept-Language. Unknown or invalid values are served as en. |
Responses
Section titled “Responses”Returns the plans available to the caller.
{ "statusCode": 200, "message": "OK", "data": { "requested_locale": "en", "plans": [ { "id": "507f1f77bcf86cd799439400", "family_id": "plan_std_8_32_500", "type": "standard", "sort_order": 10, "price_cents_per_month": 5000, "hold_days": 7, "resources": { "cpu_cores": 8, "ram_gb": 32, "disk_limit_gb": 500, "disk_io_read_mib": 1024, "disk_io_write_mib": 512, "network_limit_mbps": 1000, "max_containers": 50, "snapshots_per_container": 10, "ai_multiplier": 1 }, "text": { "title": "Standard 8", "description": "8 vCPU, 32 GB RAM, 500 GB NVMe.", "type_label": "Standard", "features": ["Dedicated resources", "Full root access", "Snapshots included"] }, "text_locales": { "title": "en", "description": "en", "type_label": "en", "features": "en" } } ] }}Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}Paid subserver subscriptions
Section titled “Paid subserver subscriptions”The paid subserver subscriptions you own. Quote, purchase, pay held subscriptions to resume them, upgrade to a larger plan, cancel, and toggle auto-renew. Purchase, pay, and upgrade return 202 with an operation to poll for progress.
GET /api/v1/subserver-operations/{id}
Section titled “GET /api/v1/subserver-operations/{id}”Status of one paid subserver operation (purchase, upgrade, resume, hold, wipe).
curl -X GET 'https://api.hoody.com/api/v1/subserver-operations/507f1f77bcf86cd799439100' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.servers.jobs.get('507f1f77bcf86cd799439100');{ "statusCode": 200, "message": "OK", "data": { "id": "507f1f77bcf86cd799439100", "subscription_id": "507f1f77bcf86cd799439200", "kind": "purchase", "target_plan_id": "507f1f77bcf86cd799439400", "step": "delivering", "result": null, "charge_cents": 5000, "created_at": "2025-01-21T22:00:00.000Z", "completed_at": null }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The operation identifier |
Responses
Section titled “Responses”Returns the operation’s current state.
{ "statusCode": 200, "message": "OK", "data": { "id": "507f1f77bcf86cd799439100", "subscription_id": "507f1f77bcf86cd799439200", "kind": "purchase", "target_plan_id": "507f1f77bcf86cd799439400", "step": "delivering", "result": null, "charge_cents": 5000, "created_at": "2025-01-21T22:00:00.000Z", "completed_at": null }}No operation with this id is visible to the caller.
{ "statusCode": 404, "error": "SUBSERVER_OPERATION_NOT_FOUND", "message": "Operation not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
SUBSERVER_OPERATION_NOT_FOUND | No such operation | No paid subserver operation with this id is visible to the caller. | Use the operation id returned by the purchase, upgrade or pay call. |
Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}GET /api/v1/subserver-subscriptions
Section titled “GET /api/v1/subserver-subscriptions”List every paid subserver subscription you own.
curl -X GET 'https://api.hoody.com/api/v1/subserver-subscriptions?limit=50&offset=0&locale=en' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.servers.subscriptions.list({ limit: 50, offset: 0, locale: 'en' });{ "statusCode": 200, "message": "OK", "data": [ { "id": "507f1f77bcf86cd799439200", "plan_id": "507f1f77bcf86cd799439400", "state": "active", "server_id": "507f1f77bcf86cd799439210", "server_name": "node-example-1", "anchor_day": 21, "auto_renew": true, "paid_through": "2025-02-21T22:00:00.000Z", "active_operation_id": null, "delete_at": null, "plan": { "family_id": "plan_std_8_32_500", "title": "Standard 8", "type_label": "Standard", "price_cents_per_month": 5000, "hold_days": 7, "resources": { "cpu_cores": 8, "ram_gb": 32, "disk_limit_gb": 500, "disk_io_read_mib": 1024, "disk_io_write_mib": 512, "network_limit_mbps": 1000, "max_containers": 50, "snapshots_per_container": 10, "ai_multiplier": 1 } }, "created_at": "2025-01-21T22:00:00.000Z", "updated_at": "2025-01-21T22:30:00.000Z" } ], "total": 1, "limit": 50, "offset": 0}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
limit | query | integer | No | Maximum number of subscriptions to return. Default: 50. |
offset | query | integer | No | Number of subscriptions to skip. Default: 0. |
locale | query | string | No | Language tag for plan.title and plan.type_label, such as en, fr or pt-BR. Overrides Accept-Language. Unknown or invalid values are served as en. |
Responses
Section titled “Responses”Returns the caller’s subscriptions with pagination totals.
{ "statusCode": 200, "message": "OK", "data": [ { "id": "507f1f77bcf86cd799439200", "plan_id": "507f1f77bcf86cd799439400", "state": "active", "server_id": "507f1f77bcf86cd799439210", "server_name": "node-example-1", "anchor_day": 21, "auto_renew": true, "paid_through": "2025-02-21T22:00:00.000Z", "active_operation_id": null, "delete_at": null, "plan": { "family_id": "plan_std_8_32_500", "title": "Standard 8", "type_label": "Standard", "price_cents_per_month": 5000, "hold_days": 7, "resources": { "cpu_cores": 8, "ram_gb": 32, "disk_limit_gb": 500, "disk_io_read_mib": 1024, "disk_io_write_mib": 512, "network_limit_mbps": 1000, "max_containers": 50, "snapshots_per_container": 10, "ai_multiplier": 1 } }, "created_at": "2025-01-21T22:00:00.000Z", "updated_at": "2025-01-21T22:30:00.000Z" } ], "total": 1, "limit": 50, "offset": 0}Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}GET /api/v1/subserver-subscriptions/{id}
Section titled “GET /api/v1/subserver-subscriptions/{id}”Get one paid subserver subscription you own.
curl -X GET 'https://api.hoody.com/api/v1/subserver-subscriptions/507f1f77bcf86cd799439200?locale=en' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.servers.subscriptions.get('507f1f77bcf86cd799439200', { locale: 'en' });{ "statusCode": 200, "message": "OK", "data": { "id": "507f1f77bcf86cd799439200", "plan_id": "507f1f77bcf86cd799439400", "state": "active", "server_id": "507f1f77bcf86cd799439210", "server_name": "node-example-1", "anchor_day": 21, "auto_renew": true, "paid_through": "2025-02-21T22:00:00.000Z", "active_operation_id": null, "delete_at": null, "plan": { "family_id": "plan_std_8_32_500", "title": "Standard 8", "type_label": "Standard", "price_cents_per_month": 5000, "hold_days": 7, "resources": { "cpu_cores": 8, "ram_gb": 32, "disk_limit_gb": 500, "disk_io_read_mib": 1024, "disk_io_write_mib": 512, "network_limit_mbps": 1000, "max_containers": 50, "snapshots_per_container": 10, "ai_multiplier": 1 } }, "created_at": "2025-01-21T22:00:00.000Z", "updated_at": "2025-01-21T22:30:00.000Z" }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The subscription identifier |
locale | query | string | No | Language tag for plan.title and plan.type_label, such as en, fr or pt-BR. Overrides Accept-Language. Unknown or invalid values are served as en. |
Responses
Section titled “Responses”Returns the subscription.
{ "statusCode": 200, "message": "OK", "data": { "id": "507f1f77bcf86cd799439200", "plan_id": "507f1f77bcf86cd799439400", "state": "active", "server_id": "507f1f77bcf86cd799439210", "server_name": "node-example-1", "anchor_day": 21, "auto_renew": true, "paid_through": "2025-02-21T22:00:00.000Z", "active_operation_id": null, "delete_at": null, "plan": { "family_id": "plan_std_8_32_500", "title": "Standard 8", "type_label": "Standard", "price_cents_per_month": 5000, "hold_days": 7, "resources": { "cpu_cores": 8, "ram_gb": 32, "disk_limit_gb": 500, "disk_io_read_mib": 1024, "disk_io_write_mib": 512, "network_limit_mbps": 1000, "max_containers": 50, "snapshots_per_container": 10, "ai_multiplier": 1 } }, "created_at": "2025-01-21T22:00:00.000Z", "updated_at": "2025-01-21T22:30:00.000Z" }}No subscription with this id is visible to the caller.
{ "statusCode": 404, "error": "SUBSCRIPTION_NOT_FOUND", "message": "Subscription not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
SUBSCRIPTION_NOT_FOUND | No such subscription | No paid subserver subscription with this id is visible to the caller. | Check the id against GET /api/v1/subserver-subscriptions. |
Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}GET /api/v1/subserver-subscriptions/quote
Section titled “GET /api/v1/subserver-subscriptions/quote”Quote the cost of buying a specific plan.
curl -X GET 'https://api.hoody.com/api/v1/subserver-subscriptions/quote?plan_id=507f1f77bcf86cd799439400' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.servers.plans.quote({ plan_id: '507f1f77bcf86cd799439400' });{ "statusCode": 200, "message": "OK", "data": { "action": "purchase", "plan_id": "507f1f77bcf86cd799439400", "subscription_id": null, "kind": null, "charge_cents": 5000, "price_cents_per_month": 5000, "recovery": false, "paid_through": null, "cap": { "max": 5, "used": 0 }, "available": true }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
plan_id | query | string | Yes | The plan version id from GET /api/v1/subserver-plans. |
Responses
Section titled “Responses”Returns the price, the account’s paid subserver cap, and whether a host has room.
{ "statusCode": 200, "message": "OK", "data": { "action": "purchase", "plan_id": "507f1f77bcf86cd799439400", "subscription_id": null, "kind": null, "charge_cents": 5000, "price_cents_per_month": 5000, "recovery": false, "paid_through": null, "cap": { "max": 5, "used": 0 }, "available": true }}The purchase cannot proceed. Nothing was charged.
{ "statusCode": 409, "error": "PLAN_NOT_AVAILABLE", "message": "This plan is not available."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PLAN_NOT_AVAILABLE | Plan not available | The plan version is retired, hidden or unknown, so it cannot be bought or upgraded to. Nothing was charged. | Pick a plan from GET /api/v1/subserver-plans. |
PLAN_CHANGE_NOT_UPGRADE | Plan change is not an upgrade | A plan change must keep or raise every resource. data.lower_fields lists the resources the target plan would lower. Nothing was charged. | To go smaller, buy a smaller subserver, move your data, then cancel the bigger one. |
SUBSCRIPTION_PERIOD_ENDED | Billing period has ended | The subscription is paid only up to data.paid_through, which has passed, so an upgrade has nothing left to prorate. Nothing was charged. | Renew or pay the subscription first, then upgrade. |
SUBSCRIPTION_NOT_PAYABLE | Nothing to pay | The subscription is in a state where a payment has no effect (data.state), or it is closed. Nothing was charged. | Read the subscription. A held subscription can be paid until its deletion starts. A deleted one cannot. An active one renews by itself. |
SUBSERVER_WIPE_IN_PROGRESS | Subserver is being wiped | The subserver is being permanently deleted; the subscription can no longer be paid for. Nothing was charged. | None. Contact support. |
NO_PAID_CAPACITY | No host has room | No host open for paid subservers has room for this plan, or the host of this subserver has no room for the upgrade. Nothing was charged. | Try again later or pick a smaller plan. |
HOST_UNAVAILABLE | Host unavailable | The host of this subserver is not ready, so it cannot be changed or resumed now. Nothing was charged. | Try again later. |
PAID_SUBSERVER_LIMIT_REACHED | Paid subserver limit reached | The account already has as many paid subservers as its limit allows. data.max is the limit and data.used the paid subservers counted toward it. Nothing was charged. | Contact support to raise the limit. GET /api/v1/users/me shows the limit and the count under limits.paid_subservers. |
SUBSERVER_DORMANT | Subserver is not active | The container is on a paid subserver whose subscription is not active (on hold, being delivered, or being deleted) or has a change in progress, so containers on it cannot be created, started, stopped, changed, copied, shared or deleted. data.reason is not_active, operation_in_flight or no_subscription. Nothing was changed. | Retry once the subserver is active again (renew a held subscription, or wait for the change to finish). |
IDEMPOTENCY_INTENT_MISMATCH | Idempotency key reused for a different purchase | The supplied idempotency_key was already used for a reservation with different terms (a different offer, duration, pool or confirmed total). Returning the original would silently give the caller something other than what this request asked for, so it fails instead. | Use a fresh idempotency_key for a genuinely new purchase. |
CHARGE_CONFIRMATION_REQUIRED | The total must be confirmed before money moves | The request would debit an account and carried no max_charge_cents, so there was nothing to bind the quoted price to the charged one. Nothing was charged. This applies to every paid purchase, including servers with no one-time setup fee. | Read the current total, show it to the customer, and retry with max_charge_cents set to the value they confirmed. Never auto-retry with the total from this error: that defeats the confirmation it exists to obtain. |
CHARGE_EXCEEDS_MAX | Price changed since the quote | The live total (rental price plus any one-time setup fee) is higher than the max_charge_cents ceiling supplied with the request, so nothing was charged. | Re-read the listing, show the user the new total, and retry with an updated max_charge_cents once they confirm. Do NOT auto-retry with the returned actual value. |
Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}GET /api/v1/subserver-subscriptions/{id}/quote
Section titled “GET /api/v1/subserver-subscriptions/{id}/quote”Quote an upgrade or a payment for an existing subscription. action=upgrade with plan_id prices the upgrade for the rest of the period; action=pay prices a late payment.
curl -X GET 'https://api.hoody.com/api/v1/subserver-subscriptions/507f1f77bcf86cd799439200/quote?action=upgrade&plan_id=507f1f77bcf86cd799439400' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.servers.subscriptions.quote('507f1f77bcf86cd799439200', { action: 'upgrade', plan_id: '507f1f77bcf86cd799439400' });{ "statusCode": 200, "message": "OK", "data": { "action": "upgrade", "plan_id": "507f1f77bcf86cd799439400", "subscription_id": "507f1f77bcf86cd799439200", "kind": null, "charge_cents": 2500, "price_cents_per_month": 5000, "recovery": false, "paid_through": "2025-02-21T22:00:00.000Z", "cap": { "max": 5, "used": 1 }, "available": false }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The subscription identifier |
action | query | string | Yes | What to quote. One of upgrade, pay. |
plan_id | query | string | No | Required for action=upgrade. The target plan version id. |
Responses
Section titled “Responses”Returns the price of the requested change.
{ "statusCode": 200, "message": "OK", "data": { "action": "upgrade", "plan_id": "507f1f77bcf86cd799439400", "subscription_id": "507f1f77bcf86cd799439200", "kind": null, "charge_cents": 2500, "price_cents_per_month": 5000, "recovery": false, "paid_through": "2025-02-21T22:00:00.000Z", "cap": { "max": 5, "used": 1 }, "available": false }}No subscription with this id is visible to the caller.
{ "statusCode": 404, "error": "SUBSCRIPTION_NOT_FOUND", "message": "Subscription not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
SUBSCRIPTION_NOT_FOUND | No such subscription | No paid subserver subscription with this id is visible to the caller. | Check the id against GET /api/v1/subserver-subscriptions. |
The change cannot proceed. Nothing was charged.
{ "statusCode": 409, "error": "PLAN_NOT_AVAILABLE", "message": "This plan is not available."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PLAN_NOT_AVAILABLE | Plan not available | The plan version is retired, hidden or unknown, so it cannot be bought or upgraded to. Nothing was charged. | Pick a plan from GET /api/v1/subserver-plans. |
PLAN_CHANGE_NOT_UPGRADE | Plan change is not an upgrade | A plan change must keep or raise every resource. data.lower_fields lists the resources the target plan would lower. Nothing was charged. | To go smaller, buy a smaller subserver, move your data, then cancel the bigger one. |
SUBSCRIPTION_PERIOD_ENDED | Billing period has ended | The subscription is paid only up to data.paid_through, which has passed, so an upgrade has nothing left to prorate. Nothing was charged. | Renew or pay the subscription first, then upgrade. |
SUBSCRIPTION_NOT_PAYABLE | Nothing to pay | The subscription is in a state where a payment has no effect (data.state), or it is closed. Nothing was charged. | Read the subscription. A held subscription can be paid until its deletion starts. A deleted one cannot. An active one renews by itself. |
SUBSERVER_WIPE_IN_PROGRESS | Subserver is being wiped | The subserver is being permanently deleted; the subscription can no longer be paid for. Nothing was charged. | None. Contact support. |
NO_PAID_CAPACITY | No host has room | No host open for paid subservers has room for this plan, or the host of this subserver has no room for the upgrade. Nothing was charged. | Try again later or pick a smaller plan. |
HOST_UNAVAILABLE | Host unavailable | The host of this subserver is not ready, so it cannot be changed or resumed now. Nothing was charged. | Try again later. |
PAID_SUBSERVER_LIMIT_REACHED | Paid subserver limit reached | The account already has as many paid subservers as its limit allows. data.max is the limit and data.used the paid subservers counted toward it. Nothing was charged. | Contact support to raise the limit. GET /api/v1/users/me shows the limit and the count under limits.paid_subservers. |
SUBSERVER_DORMANT | Subserver is not active | The container is on a paid subserver whose subscription is not active (on hold, being delivered, or being deleted) or has a change in progress, so containers on it cannot be created, started, stopped, changed, copied, shared or deleted. data.reason is not_active, operation_in_flight or no_subscription. Nothing was changed. | Retry once the subserver is active again (renew a held subscription, or wait for the change to finish). |
IDEMPOTENCY_INTENT_MISMATCH | Idempotency key reused for a different purchase | The supplied idempotency_key was already used for a reservation with different terms (a different offer, duration, pool or confirmed total). Returning the original would silently give the caller something other than what this request asked for, so it fails instead. | Use a fresh idempotency_key for a genuinely new purchase. |
CHARGE_CONFIRMATION_REQUIRED | The total must be confirmed before money moves | The request would debit an account and carried no max_charge_cents, so there was nothing to bind the quoted price to the charged one. Nothing was charged. This applies to every paid purchase, including servers with no one-time setup fee. | Read the current total, show it to the customer, and retry with max_charge_cents set to the value they confirmed. Never auto-retry with the total from this error: that defeats the confirmation it exists to obtain. |
CHARGE_EXCEEDS_MAX | Price changed since the quote | The live total (rental price plus any one-time setup fee) is higher than the max_charge_cents ceiling supplied with the request, so nothing was charged. | Re-read the listing, show the user the new total, and retry with an updated max_charge_cents once they confirm. Do NOT auto-retry with the returned actual value. |
Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}POST /api/v1/subserver-subscriptions
Section titled “POST /api/v1/subserver-subscriptions”Buy a paid subserver. Charges the first month from your wallet and starts delivering the subserver. Quote first and pass max_charge_cents; a retried request with the same idempotency_key returns the original operation without charging again.
curl -X POST 'https://api.hoody.com/api/v1/subserver-subscriptions' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "plan_id": "507f1f77bcf86cd799439400", "idempotency_key": "sub-2025-01-21-7f3a", "max_charge_cents": 5000 }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.servers.subscriptions.buy({ plan_id: '507f1f77bcf86cd799439400', idempotency_key: 'sub-2025-01-21-7f3a', max_charge_cents: 5000});{ "statusCode": 202, "message": "OK", "data": { "operation": { "id": "507f1f77bcf86cd799439100", "subscription_id": "507f1f77bcf86cd799439200", "kind": "purchase", "target_plan_id": "507f1f77bcf86cd799439400", "step": "queued", "result": null, "charge_cents": 5000, "created_at": "2025-01-21T22:00:00.000Z", "completed_at": null }, "subscription": { "id": "507f1f77bcf86cd799439200", "plan_id": "507f1f77bcf86cd799439400", "state": "provisioning", "server_id": null, "server_name": null, "anchor_day": 21, "auto_renew": true, "paid_through": null, "active_operation_id": "507f1f77bcf86cd799439100", "delete_at": null, "plan": { "family_id": "plan_std_8_32_500", "title": "Standard 8", "type_label": "Standard", "price_cents_per_month": 5000, "hold_days": 7, "resources": { "cpu_cores": 8, "ram_gb": 32, "disk_limit_gb": 500, "disk_io_read_mib": 1024, "disk_io_write_mib": 512, "network_limit_mbps": 1000, "max_containers": 50, "snapshots_per_container": 10, "ai_multiplier": 1 } }, "created_at": "2025-01-21T22:00:00.000Z", "updated_at": "2025-01-21T22:00:00.000Z" }, "replayed": false }}This endpoint takes no parameters.
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
plan_id | string | Yes | Plan version id from GET /subserver-plans. |
idempotency_key | string | Yes | Caller-generated. Replaying it returns the original operation, charged once. A refused request’s key stays refused (the same error every time): use a new key to try again. |
max_charge_cents | integer | No | Ceiling on the debit, from the quote. Required whenever the charge is above 0. |
Responses
Section titled “Responses”The purchase was accepted and is in progress. Poll the returned operation to see when delivery finishes.
{ "statusCode": 202, "message": "OK", "data": { "operation": { "id": "507f1f77bcf86cd799439100", "subscription_id": "507f1f77bcf86cd799439200", "kind": "purchase", "target_plan_id": "507f1f77bcf86cd799439400", "step": "queued", "result": null, "charge_cents": 5000, "created_at": "2025-01-21T22:00:00.000Z", "completed_at": null }, "subscription": { "id": "507f1f77bcf86cd799439200", "plan_id": "507f1f77bcf86cd799439400", "state": "provisioning", "server_id": null, "server_name": null, "anchor_day": 21, "auto_renew": true, "paid_through": null, "active_operation_id": "507f1f77bcf86cd799439100", "delete_at": null, "plan": { "family_id": "plan_std_8_32_500", "title": "Standard 8", "type_label": "Standard", "price_cents_per_month": 5000, "hold_days": 7, "resources": { "cpu_cores": 8, "ram_gb": 32, "disk_limit_gb": 500, "disk_io_read_mib": 1024, "disk_io_write_mib": 512, "network_limit_mbps": 1000, "max_containers": 50, "snapshots_per_container": 10, "ai_multiplier": 1 } }, "created_at": "2025-01-21T22:00:00.000Z", "updated_at": "2025-01-21T22:00:00.000Z" }, "replayed": false }}The wallet balance is below the total required.
{ "statusCode": 400, "error": "INSUFFICIENT_BALANCE", "message": "Insufficient balance for rental", "data": { "rental_cents": 20000, "setup_fee_cents": 2500, "required_cents": 22500, "available_cents": 18000 }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSUFFICIENT_BALANCE | Wallet balance too low | The wallet balance is below the total required for this rental. The response data breaks the total down into rental_cents, setup_fee_cents, required_cents and available_cents so a client can explain the shortfall. available_cents is the wallet balance, so it is omitted when the caller is an auth token without financial.wallet.read. | Top up the wallet to at least required_cents, then retry. |
No subscription with this id is visible to the caller.
{ "statusCode": 404, "error": "SUBSCRIPTION_NOT_FOUND", "message": "Subscription not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
SUBSCRIPTION_NOT_FOUND | No such subscription | No paid subserver subscription with this id is visible to the caller. | Check the id against GET /api/v1/subserver-subscriptions. |
The purchase cannot proceed. Nothing was charged.
{ "statusCode": 409, "error": "PLAN_NOT_AVAILABLE", "message": "This plan is not available."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PLAN_NOT_AVAILABLE | Plan not available | The plan version is retired, hidden or unknown, so it cannot be bought or upgraded to. Nothing was charged. | Pick a plan from GET /api/v1/subserver-plans. |
PLAN_CHANGE_NOT_UPGRADE | Plan change is not an upgrade | A plan change must keep or raise every resource. data.lower_fields lists the resources the target plan would lower. Nothing was charged. | To go smaller, buy a smaller subserver, move your data, then cancel the bigger one. |
SUBSCRIPTION_PERIOD_ENDED | Billing period has ended | The subscription is paid only up to data.paid_through, which has passed, so an upgrade has nothing left to prorate. Nothing was charged. | Renew or pay the subscription first, then upgrade. |
SUBSCRIPTION_NOT_PAYABLE | Nothing to pay | The subscription is in a state where a payment has no effect (data.state), or it is closed. Nothing was charged. | Read the subscription. A held subscription can be paid until its deletion starts. A deleted one cannot. An active one renews by itself. |
SUBSERVER_WIPE_IN_PROGRESS | Subserver is being wiped | The subserver is being permanently deleted; the subscription can no longer be paid for. Nothing was charged. | None. Contact support. |
NO_PAID_CAPACITY | No host has room | No host open for paid subservers has room for this plan, or the host of this subserver has no room for the upgrade. Nothing was charged. | Try again later or pick a smaller plan. |
HOST_UNAVAILABLE | Host unavailable | The host of this subserver is not ready, so it cannot be changed or resumed now. Nothing was charged. | Try again later. |
PAID_SUBSERVER_LIMIT_REACHED | Paid subserver limit reached | The account already has as many paid subservers as its limit allows. data.max is the limit and data.used the paid subservers counted toward it. Nothing was charged. | Contact support to raise the limit. GET /api/v1/users/me shows the limit and the count under limits.paid_subservers. |
SUBSERVER_DORMANT | Subserver is not active | The container is on a paid subserver whose subscription is not active (on hold, being delivered, or being deleted) or has a change in progress, so containers on it cannot be created, started, stopped, changed, copied, shared or deleted. data.reason is not_active, operation_in_flight or no_subscription. Nothing was changed. | Retry once the subserver is active again (renew a held subscription, or wait for the change to finish). |
IDEMPOTENCY_INTENT_MISMATCH | Idempotency key reused for a different purchase | The supplied idempotency_key was already used for a reservation with different terms (a different offer, duration, pool or confirmed total). Returning the original would silently give the caller something other than what this request asked for, so it fails instead. | Use a fresh idempotency_key for a genuinely new purchase. |
CHARGE_CONFIRMATION_REQUIRED | The total must be confirmed before money moves | The request would debit an account and carried no max_charge_cents, so there was nothing to bind the quoted price to the charged one. Nothing was charged. This applies to every paid purchase, including servers with no one-time setup fee. | Read the current total, show it to the customer, and retry with max_charge_cents set to the value they confirmed. Never auto-retry with the total from this error: that defeats the confirmation it exists to obtain. |
CHARGE_EXCEEDS_MAX | Price changed since the quote | The live total (rental price plus any one-time setup fee) is higher than the max_charge_cents ceiling supplied with the request, so nothing was charged. | Re-read the listing, show the user the new total, and retry with an updated max_charge_cents once they confirm. Do NOT auto-retry with the returned actual value. |
Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}POST /api/v1/subserver-subscriptions/{id}/cancel
Section titled “POST /api/v1/subserver-subscriptions/{id}/cancel”Cancel a paid subserver subscription. Turns auto-renew off. The subserver keeps running until paid_through, then goes on hold. No refund.
curl -X POST 'https://api.hoody.com/api/v1/subserver-subscriptions/507f1f77bcf86cd799439200/cancel' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.servers.subscriptions.cancel('507f1f77bcf86cd799439200');{ "statusCode": 200, "message": "OK", "data": { "id": "507f1f77bcf86cd799439200", "plan_id": "507f1f77bcf86cd799439400", "state": "active", "server_id": "507f1f77bcf86cd799439210", "server_name": "node-example-1", "anchor_day": 21, "auto_renew": false, "paid_through": "2025-02-21T22:00:00.000Z", "active_operation_id": null, "delete_at": null, "plan": { "family_id": "plan_std_8_32_500", "title": "Standard 8", "type_label": "Standard", "price_cents_per_month": 5000, "hold_days": 7, "resources": { "cpu_cores": 8, "ram_gb": 32, "disk_limit_gb": 500, "disk_io_read_mib": 1024, "disk_io_write_mib": 512, "network_limit_mbps": 1000, "max_containers": 50, "snapshots_per_container": 10, "ai_multiplier": 1 } }, "created_at": "2025-01-21T22:00:00.000Z", "updated_at": "2025-01-21T22:30:00.000Z" }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The subscription identifier |
Responses
Section titled “Responses”Returns the subscription with auto_renew set to false.
{ "statusCode": 200, "message": "OK", "data": { "id": "507f1f77bcf86cd799439200", "plan_id": "507f1f77bcf86cd799439400", "state": "active", "server_id": "507f1f77bcf86cd799439210", "server_name": "node-example-1", "anchor_day": 21, "auto_renew": false, "paid_through": "2025-02-21T22:00:00.000Z", "active_operation_id": null, "delete_at": null, "plan": { "family_id": "plan_std_8_32_500", "title": "Standard 8", "type_label": "Standard", "price_cents_per_month": 5000, "hold_days": 7, "resources": { "cpu_cores": 8, "ram_gb": 32, "disk_limit_gb": 500, "disk_io_read_mib": 1024, "disk_io_write_mib": 512, "network_limit_mbps": 1000, "max_containers": 50, "snapshots_per_container": 10, "ai_multiplier": 1 } }, "created_at": "2025-01-21T22:00:00.000Z", "updated_at": "2025-01-21T22:30:00.000Z" }}No subscription with this id is visible to the caller.
{ "statusCode": 404, "error": "SUBSCRIPTION_NOT_FOUND", "message": "Subscription not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
SUBSCRIPTION_NOT_FOUND | No such subscription | No paid subserver subscription with this id is visible to the caller. | Check the id against GET /api/v1/subserver-subscriptions. |
The cancel could not be applied. Nothing was charged.
{ "statusCode": 409, "error": "SUBSCRIPTION_NOT_PAYABLE", "message": "This subscription has nothing to pay right now."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PLAN_NOT_AVAILABLE | Plan not available | The plan version is retired, hidden or unknown, so it cannot be bought or upgraded to. Nothing was charged. | Pick a plan from GET /api/v1/subserver-plans. |
PLAN_CHANGE_NOT_UPGRADE | Plan change is not an upgrade | A plan change must keep or raise every resource. data.lower_fields lists the resources the target plan would lower. Nothing was charged. | To go smaller, buy a smaller subserver, move your data, then cancel the bigger one. |
SUBSCRIPTION_PERIOD_ENDED | Billing period has ended | The subscription is paid only up to data.paid_through, which has passed, so an upgrade has nothing left to prorate. Nothing was charged. | Renew or pay the subscription first, then upgrade. |
SUBSCRIPTION_NOT_PAYABLE | Nothing to pay | The subscription is in a state where a payment has no effect (data.state), or it is closed. Nothing was charged. | Read the subscription. A held subscription can be paid until its deletion starts. A deleted one cannot. An active one renews by itself. |
SUBSERVER_WIPE_IN_PROGRESS | Subserver is being wiped | The subserver is being permanently deleted; the subscription can no longer be paid for. Nothing was charged. | None. Contact support. |
NO_PAID_CAPACITY | No host has room | No host open for paid subservers has room for this plan, or the host of this subserver has no room for the upgrade. Nothing was charged. | Try again later or pick a smaller plan. |
HOST_UNAVAILABLE | Host unavailable | The host of this subserver is not ready, so it cannot be changed or resumed now. Nothing was charged. | Try again later. |
PAID_SUBSERVER_LIMIT_REACHED | Paid subserver limit reached | The account already has as many paid subservers as its limit allows. data.max is the limit and data.used the paid subservers counted toward it. Nothing was charged. | Contact support to raise the limit. GET /api/v1/users/me shows the limit and the count under limits.paid_subservers. |
SUBSERVER_DORMANT | Subserver is not active | The container is on a paid subserver whose subscription is not active (on hold, being delivered, or being deleted) or has a change in progress, so containers on it cannot be created, started, stopped, changed, copied, shared or deleted. data.reason is not_active, operation_in_flight or no_subscription. Nothing was changed. | Retry once the subserver is active again (renew a held subscription, or wait for the change to finish). |
IDEMPOTENCY_INTENT_MISMATCH | Idempotency key reused for a different purchase | The supplied idempotency_key was already used for a reservation with different terms (a different offer, duration, pool or confirmed total). Returning the original would silently give the caller something other than what this request asked for, so it fails instead. | Use a fresh idempotency_key for a genuinely new purchase. |
CHARGE_CONFIRMATION_REQUIRED | The total must be confirmed before money moves | The request would debit an account and carried no max_charge_cents, so there was nothing to bind the quoted price to the charged one. Nothing was charged. This applies to every paid purchase, including servers with no one-time setup fee. | Read the current total, show it to the customer, and retry with max_charge_cents set to the value they confirmed. Never auto-retry with the total from this error: that defeats the confirmation it exists to obtain. |
CHARGE_EXCEEDS_MAX | Price changed since the quote | The live total (rental price plus any one-time setup fee) is higher than the max_charge_cents ceiling supplied with the request, so nothing was charged. | Re-read the listing, show the user the new total, and retry with an updated max_charge_cents once they confirm. Do NOT auto-retry with the returned actual value. |
Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}POST /api/v1/subserver-subscriptions/{id}/pay
Section titled “POST /api/v1/subserver-subscriptions/{id}/pay”Pay a held subscription and resume it. Charges one month starting today (the renewal day moves to today) and resumes the subserver from its host. Months spent on hold are never charged. A failed paid recovery is retried at no charge. A subserver permanently deleted after its hold (wiping, or since deleted) cannot be paid for.
curl -X POST 'https://api.hoody.com/api/v1/subserver-subscriptions/507f1f77bcf86cd799439200/pay' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "idempotency_key": "pay-2025-01-21-7f3a", "max_charge_cents": 5000 }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.servers.subscriptions.pay('507f1f77bcf86cd799439200', { idempotency_key: 'pay-2025-01-21-7f3a', max_charge_cents: 5000});{ "statusCode": 202, "message": "OK", "data": { "operation": { "id": "507f1f77bcf86cd799439101", "subscription_id": "507f1f77bcf86cd799439200", "kind": "resume", "target_plan_id": "507f1f77bcf86cd799439400", "step": "resuming", "result": null, "charge_cents": 5000, "created_at": "2025-01-21T22:00:00.000Z", "completed_at": null }, "subscription": { "id": "507f1f77bcf86cd799439200", "plan_id": "507f1f77bcf86cd799439400", "state": "active", "server_id": "507f1f77bcf86cd799439210", "server_name": "node-example-1", "anchor_day": 21, "auto_renew": true, "paid_through": "2025-02-21T22:00:00.000Z", "active_operation_id": "507f1f77bcf86cd799439101", "delete_at": null, "plan": { "family_id": "plan_std_8_32_500", "title": "Standard 8", "type_label": "Standard", "price_cents_per_month": 5000, "hold_days": 7, "resources": { "cpu_cores": 8, "ram_gb": 32, "disk_limit_gb": 500, "disk_io_read_mib": 1024, "disk_io_write_mib": 512, "network_limit_mbps": 1000, "max_containers": 50, "snapshots_per_container": 10, "ai_multiplier": 1 } }, "created_at": "2025-01-21T22:00:00.000Z", "updated_at": "2025-01-21T22:30:00.000Z" }, "replayed": false }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The subscription identifier |
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
idempotency_key | string | Yes | Caller-generated. Replaying it returns the original operation, charged once. A refused request’s key stays refused (the same error every time): use a new key to try again. |
max_charge_cents | integer | No | Ceiling on the debit, from the quote. Required whenever the charge is above 0. |
Responses
Section titled “Responses”The payment was accepted and the subserver is resuming. Poll the returned operation.
{ "statusCode": 202, "message": "OK", "data": { "operation": { "id": "507f1f77bcf86cd799439101", "subscription_id": "507f1f77bcf86cd799439200", "kind": "resume", "target_plan_id": "507f1f77bcf86cd799439400", "step": "resuming", "result": null, "charge_cents": 5000, "created_at": "2025-01-21T22:00:00.000Z", "completed_at": null }, "subscription": { "id": "507f1f77bcf86cd799439200", "plan_id": "507f1f77bcf86cd799439400", "state": "active", "server_id": "507f1f77bcf86cd799439210", "server_name": "node-example-1", "anchor_day": 21, "auto_renew": true, "paid_through": "2025-02-21T22:00:00.000Z", "active_operation_id": "507f1f77bcf86cd799439101", "delete_at": null, "plan": { "family_id": "plan_std_8_32_500", "title": "Standard 8", "type_label": "Standard", "price_cents_per_month": 5000, "hold_days": 7, "resources": { "cpu_cores": 8, "ram_gb": 32, "disk_limit_gb": 500, "disk_io_read_mib": 1024, "disk_io_write_mib": 512, "network_limit_mbps": 1000, "max_containers": 50, "snapshots_per_container": 10, "ai_multiplier": 1 } }, "created_at": "2025-01-21T22:00:00.000Z", "updated_at": "2025-01-21T22:30:00.000Z" }, "replayed": false }}The wallet balance is below the total required.
{ "statusCode": 400, "error": "INSUFFICIENT_BALANCE", "message": "Insufficient balance for rental", "data": { "rental_cents": 20000, "setup_fee_cents": 2500, "required_cents": 22500, "available_cents": 18000 }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSUFFICIENT_BALANCE | Wallet balance too low | The wallet balance is below the total required for this rental. The response data breaks the total down into rental_cents, setup_fee_cents, required_cents and available_cents so a client can explain the shortfall. available_cents is the wallet balance, so it is omitted when the caller is an auth token without financial.wallet.read. | Top up the wallet to at least required_cents, then retry. |
No subscription with this id is visible to the caller.
{ "statusCode": 404, "error": "SUBSCRIPTION_NOT_FOUND", "message": "Subscription not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
SUBSCRIPTION_NOT_FOUND | No such subscription | No paid subserver subscription with this id is visible to the caller. | Check the id against GET /api/v1/subserver-subscriptions. |
The payment cannot proceed. Nothing was charged.
{ "statusCode": 409, "error": "SUBSCRIPTION_NOT_PAYABLE", "message": "This subscription has nothing to pay right now."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PLAN_NOT_AVAILABLE | Plan not available | The plan version is retired, hidden or unknown, so it cannot be bought or upgraded to. Nothing was charged. | Pick a plan from GET /api/v1/subserver-plans. |
PLAN_CHANGE_NOT_UPGRADE | Plan change is not an upgrade | A plan change must keep or raise every resource. data.lower_fields lists the resources the target plan would lower. Nothing was charged. | To go smaller, buy a smaller subserver, move your data, then cancel the bigger one. |
SUBSCRIPTION_PERIOD_ENDED | Billing period has ended | The subscription is paid only up to data.paid_through, which has passed, so an upgrade has nothing left to prorate. Nothing was charged. | Renew or pay the subscription first, then upgrade. |
SUBSCRIPTION_NOT_PAYABLE | Nothing to pay | The subscription is in a state where a payment has no effect (data.state), or it is closed. Nothing was charged. | Read the subscription. A held subscription can be paid until its deletion starts. A deleted one cannot. An active one renews by itself. |
SUBSERVER_WIPE_IN_PROGRESS | Subserver is being wiped | The subserver is being permanently deleted; the subscription can no longer be paid for. Nothing was charged. | None. Contact support. |
NO_PAID_CAPACITY | No host has room | No host open for paid subservers has room for this plan, or the host of this subserver has no room for the upgrade. Nothing was charged. | Try again later or pick a smaller plan. |
HOST_UNAVAILABLE | Host unavailable | The host of this subserver is not ready, so it cannot be changed or resumed now. Nothing was charged. | Try again later. |
PAID_SUBSERVER_LIMIT_REACHED | Paid subserver limit reached | The account already has as many paid subservers as its limit allows. data.max is the limit and data.used the paid subservers counted toward it. Nothing was charged. | Contact support to raise the limit. GET /api/v1/users/me shows the limit and the count under limits.paid_subservers. |
SUBSERVER_DORMANT | Subserver is not active | The container is on a paid subserver whose subscription is not active (on hold, being delivered, or being deleted) or has a change in progress, so containers on it cannot be created, started, stopped, changed, copied, shared or deleted. data.reason is not_active, operation_in_flight or no_subscription. Nothing was changed. | Retry once the subserver is active again (renew a held subscription, or wait for the change to finish). |
IDEMPOTENCY_INTENT_MISMATCH | Idempotency key reused for a different purchase | The supplied idempotency_key was already used for a reservation with different terms (a different offer, duration, pool or confirmed total). Returning the original would silently give the caller something other than what this request asked for, so it fails instead. | Use a fresh idempotency_key for a genuinely new purchase. |
CHARGE_CONFIRMATION_REQUIRED | The total must be confirmed before money moves | The request would debit an account and carried no max_charge_cents, so there was nothing to bind the quoted price to the charged one. Nothing was charged. This applies to every paid purchase, including servers with no one-time setup fee. | Read the current total, show it to the customer, and retry with max_charge_cents set to the value they confirmed. Never auto-retry with the total from this error: that defeats the confirmation it exists to obtain. |
CHARGE_EXCEEDS_MAX | Price changed since the quote | The live total (rental price plus any one-time setup fee) is higher than the max_charge_cents ceiling supplied with the request, so nothing was charged. | Re-read the listing, show the user the new total, and retry with an updated max_charge_cents once they confirm. Do NOT auto-retry with the returned actual value. |
Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}POST /api/v1/subserver-subscriptions/{id}/upgrade
Section titled “POST /api/v1/subserver-subscriptions/{id}/upgrade”Upgrade a paid subserver. Moves the subscription to a plan with every resource equal or larger. Charges only the price difference for the rest of the current period. The subserver is stopped, changed and started again (a short downtime).
curl -X POST 'https://api.hoody.com/api/v1/subserver-subscriptions/507f1f77bcf86cd799439200/upgrade' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "plan_id": "507f1f77bcf86cd799439401", "idempotency_key": "upg-2025-01-21-7f3a", "max_charge_cents": 2500 }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.servers.subscriptions.upgrade('507f1f77bcf86cd799439200', { plan_id: '507f1f77bcf86cd799439401', idempotency_key: 'upg-2025-01-21-7f3a', max_charge_cents: 2500});{ "statusCode": 202, "message": "OK", "data": { "operation": { "id": "507f1f77bcf86cd799439102", "subscription_id": "507f1f77bcf86cd799439200", "kind": "upgrade", "target_plan_id": "507f1f77bcf86cd799439401", "step": "queued", "result": null, "charge_cents": 2500, "created_at": "2025-01-21T22:00:00.000Z", "completed_at": null }, "subscription": { "id": "507f1f77bcf86cd799439200", "plan_id": "507f1f77bcf86cd799439401", "state": "active", "server_id": "507f1f77bcf86cd799439210", "server_name": "node-example-1", "anchor_day": 21, "auto_renew": true, "paid_through": "2025-02-21T22:00:00.000Z", "active_operation_id": "507f1f77bcf86cd799439102", "delete_at": null, "plan": { "family_id": "plan_std_16_64_1000", "title": "Standard 16", "type_label": "Standard", "price_cents_per_month": 9000, "hold_days": 7, "resources": { "cpu_cores": 16, "ram_gb": 64, "disk_limit_gb": 1000, "disk_io_read_mib": 2048, "disk_io_write_mib": 1024, "network_limit_mbps": 2000, "max_containers": 100, "snapshots_per_container": 20, "ai_multiplier": 2 } }, "created_at": "2025-01-21T22:00:00.000Z", "updated_at": "2025-01-21T22:30:00.000Z" }, "replayed": false }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The subscription identifier |
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
plan_id | string | Yes | The target plan version id. |
idempotency_key | string | Yes | Caller-generated. Replaying it returns the original operation, charged once. A refused request’s key stays refused (the same error every time): use a new key to try again. |
max_charge_cents | integer | No | Ceiling on the debit, from the quote. Required whenever the charge is above 0. |
Responses
Section titled “Responses”The upgrade was accepted and is in progress. Poll the returned operation.
{ "statusCode": 202, "message": "OK", "data": { "operation": { "id": "507f1f77bcf86cd799439102", "subscription_id": "507f1f77bcf86cd799439200", "kind": "upgrade", "target_plan_id": "507f1f77bcf86cd799439401", "step": "queued", "result": null, "charge_cents": 2500, "created_at": "2025-01-21T22:00:00.000Z", "completed_at": null }, "subscription": { "id": "507f1f77bcf86cd799439200", "plan_id": "507f1f77bcf86cd799439401", "state": "active", "server_id": "507f1f77bcf86cd799439210", "server_name": "node-example-1", "anchor_day": 21, "auto_renew": true, "paid_through": "2025-02-21T22:00:00.000Z", "active_operation_id": "507f1f77bcf86cd799439102", "delete_at": null, "plan": { "family_id": "plan_std_16_64_1000", "title": "Standard 16", "type_label": "Standard", "price_cents_per_month": 9000, "hold_days": 7, "resources": { "cpu_cores": 16, "ram_gb": 64, "disk_limit_gb": 1000, "disk_io_read_mib": 2048, "disk_io_write_mib": 1024, "network_limit_mbps": 2000, "max_containers": 100, "snapshots_per_container": 20, "ai_multiplier": 2 } }, "created_at": "2025-01-21T22:00:00.000Z", "updated_at": "2025-01-21T22:30:00.000Z" }, "replayed": false }}The wallet balance is below the total required.
{ "statusCode": 400, "error": "INSUFFICIENT_BALANCE", "message": "Insufficient balance for rental", "data": { "rental_cents": 20000, "setup_fee_cents": 2500, "required_cents": 22500, "available_cents": 18000 }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSUFFICIENT_BALANCE | Wallet balance too low | The wallet balance is below the total required for this rental. The response data breaks the total down into rental_cents, setup_fee_cents, required_cents and available_cents so a client can explain the shortfall. available_cents is the wallet balance, so it is omitted when the caller is an auth token without financial.wallet.read. | Top up the wallet to at least required_cents, then retry. |
No subscription with this id is visible to the caller.
{ "statusCode": 404, "error": "SUBSCRIPTION_NOT_FOUND", "message": "Subscription not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
SUBSCRIPTION_NOT_FOUND | No such subscription | No paid subserver subscription with this id is visible to the caller. | Check the id against GET /api/v1/subserver-subscriptions. |
The upgrade cannot proceed. Nothing was charged.
{ "statusCode": 409, "error": "PLAN_NOT_AVAILABLE", "message": "This plan is not available."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PLAN_NOT_AVAILABLE | Plan not available | The plan version is retired, hidden or unknown, so it cannot be bought or upgraded to. Nothing was charged. | Pick a plan from GET /api/v1/subserver-plans. |
PLAN_CHANGE_NOT_UPGRADE | Plan change is not an upgrade | A plan change must keep or raise every resource. data.lower_fields lists the resources the target plan would lower. Nothing was charged. | To go smaller, buy a smaller subserver, move your data, then cancel the bigger one. |
SUBSCRIPTION_PERIOD_ENDED | Billing period has ended | The subscription is paid only up to data.paid_through, which has passed, so an upgrade has nothing left to prorate. Nothing was charged. | Renew or pay the subscription first, then upgrade. |
SUBSCRIPTION_NOT_PAYABLE | Nothing to pay | The subscription is in a state where a payment has no effect (data.state), or it is closed. Nothing was charged. | Read the subscription. A held subscription can be paid until its deletion starts. A deleted one cannot. An active one renews by itself. |
SUBSERVER_WIPE_IN_PROGRESS | Subserver is being wiped | The subserver is being permanently deleted; the subscription can no longer be paid for. Nothing was charged. | None. Contact support. |
NO_PAID_CAPACITY | No host has room | No host open for paid subservers has room for this plan, or the host of this subserver has no room for the upgrade. Nothing was charged. | Try again later or pick a smaller plan. |
HOST_UNAVAILABLE | Host unavailable | The host of this subserver is not ready, so it cannot be changed or resumed now. Nothing was charged. | Try again later. |
PAID_SUBSERVER_LIMIT_REACHED | Paid subserver limit reached | The account already has as many paid subservers as its limit allows. data.max is the limit and data.used the paid subservers counted toward it. Nothing was charged. | Contact support to raise the limit. GET /api/v1/users/me shows the limit and the count under limits.paid_subservers. |
SUBSERVER_DORMANT | Subserver is not active | The container is on a paid subserver whose subscription is not active (on hold, being delivered, or being deleted) or has a change in progress, so containers on it cannot be created, started, stopped, changed, copied, shared or deleted. data.reason is not_active, operation_in_flight or no_subscription. Nothing was changed. | Retry once the subserver is active again (renew a held subscription, or wait for the change to finish). |
IDEMPOTENCY_INTENT_MISMATCH | Idempotency key reused for a different purchase | The supplied idempotency_key was already used for a reservation with different terms (a different offer, duration, pool or confirmed total). Returning the original would silently give the caller something other than what this request asked for, so it fails instead. | Use a fresh idempotency_key for a genuinely new purchase. |
CHARGE_CONFIRMATION_REQUIRED | The total must be confirmed before money moves | The request would debit an account and carried no max_charge_cents, so there was nothing to bind the quoted price to the charged one. Nothing was charged. This applies to every paid purchase, including servers with no one-time setup fee. | Read the current total, show it to the customer, and retry with max_charge_cents set to the value they confirmed. Never auto-retry with the total from this error: that defeats the confirmation it exists to obtain. |
CHARGE_EXCEEDS_MAX | Price changed since the quote | The live total (rental price plus any one-time setup fee) is higher than the max_charge_cents ceiling supplied with the request, so nothing was charged. | Re-read the listing, show the user the new total, and retry with an updated max_charge_cents once they confirm. Do NOT auto-retry with the returned actual value. |
Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}PUT /api/v1/subserver-subscriptions/{id}/auto-renew
Section titled “PUT /api/v1/subserver-subscriptions/{id}/auto-renew”Turn auto-renew on or off. PATCH is also accepted on this endpoint with identical behaviour.
The SDK reaches this same operation through two named methods, each of which sets the auto_renew body field on the caller’s behalf:
client.api.servers.subscriptions.enableAutoRenew(id)sets bodyauto_renew=trueclient.api.servers.subscriptions.disableAutoRenew(id)sets bodyauto_renew=false
curl -X PUT 'https://api.hoody.com/api/v1/subserver-subscriptions/507f1f77bcf86cd799439200/auto-renew' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "auto_renew": false }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.servers.subscriptions.enableAutoRenew('507f1f77bcf86cd799439200');{ "statusCode": 200, "message": "OK", "data": { "id": "507f1f77bcf86cd799439200", "plan_id": "507f1f77bcf86cd799439400", "state": "active", "server_id": "507f1f77bcf86cd799439210", "server_name": "node-example-1", "anchor_day": 21, "auto_renew": false, "paid_through": "2025-02-21T22:00:00.000Z", "active_operation_id": null, "delete_at": null, "plan": { "family_id": "plan_std_8_32_500", "title": "Standard 8", "type_label": "Standard", "price_cents_per_month": 5000, "hold_days": 7, "resources": { "cpu_cores": 8, "ram_gb": 32, "disk_limit_gb": 500, "disk_io_read_mib": 1024, "disk_io_write_mib": 512, "network_limit_mbps": 1000, "max_containers": 50, "snapshots_per_container": 10, "ai_multiplier": 1 } }, "created_at": "2025-01-21T22:00:00.000Z", "updated_at": "2025-01-21T22:30:00.000Z" }}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The subscription identifier |
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
auto_renew | boolean | Yes | true renews the subscription from the wallet at the end of each paid period; false lets it stop when the current period ends. Only the account owner (signed in, not impersonated) or an auth token can turn it on. |
Responses
Section titled “Responses”Returns the subscription with the new auto_renew value.
{ "statusCode": 200, "message": "OK", "data": { "id": "507f1f77bcf86cd799439200", "plan_id": "507f1f77bcf86cd799439400", "state": "active", "server_id": "507f1f77bcf86cd799439210", "server_name": "node-example-1", "anchor_day": 21, "auto_renew": false, "paid_through": "2025-02-21T22:00:00.000Z", "active_operation_id": null, "delete_at": null, "plan": { "family_id": "plan_std_8_32_500", "title": "Standard 8", "type_label": "Standard", "price_cents_per_month": 5000, "hold_days": 7, "resources": { "cpu_cores": 8, "ram_gb": 32, "disk_limit_gb": 500, "disk_io_read_mib": 1024, "disk_io_write_mib": 512, "network_limit_mbps": 1000, "max_containers": 50, "snapshots_per_container": 10, "ai_multiplier": 1 } }, "created_at": "2025-01-21T22:00:00.000Z", "updated_at": "2025-01-21T22:30:00.000Z" }}No subscription with this id is visible to the caller.
{ "statusCode": 404, "error": "SUBSCRIPTION_NOT_FOUND", "message": "Subscription not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
SUBSCRIPTION_NOT_FOUND | No such subscription | No paid subserver subscription with this id is visible to the caller. | Check the id against GET /api/v1/subserver-subscriptions. |
The change could not be applied. Nothing was charged.
{ "statusCode": 409, "error": "SUBSCRIPTION_NOT_PAYABLE", "message": "This subscription has nothing to pay right now."}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PLAN_NOT_AVAILABLE | Plan not available | The plan version is retired, hidden or unknown, so it cannot be bought or upgraded to. Nothing was charged. | Pick a plan from GET /api/v1/subserver-plans. |
PLAN_CHANGE_NOT_UPGRADE | Plan change is not an upgrade | A plan change must keep or raise every resource. data.lower_fields lists the resources the target plan would lower. Nothing was charged. | To go smaller, buy a smaller subserver, move your data, then cancel the bigger one. |
SUBSCRIPTION_PERIOD_ENDED | Billing period has ended | The subscription is paid only up to data.paid_through, which has passed, so an upgrade has nothing left to prorate. Nothing was charged. | Renew or pay the subscription first, then upgrade. |
SUBSCRIPTION_NOT_PAYABLE | Nothing to pay | The subscription is in a state where a payment has no effect (data.state), or it is closed. Nothing was charged. | Read the subscription. A held subscription can be paid until its deletion starts. A deleted one cannot. An active one renews by itself. |
SUBSERVER_WIPE_IN_PROGRESS | Subserver is being wiped | The subserver is being permanently deleted; the subscription can no longer be paid for. Nothing was charged. | None. Contact support. |
NO_PAID_CAPACITY | No host has room | No host open for paid subservers has room for this plan, or the host of this subserver has no room for the upgrade. Nothing was charged. | Try again later or pick a smaller plan. |
HOST_UNAVAILABLE | Host unavailable | The host of this subserver is not ready, so it cannot be changed or resumed now. Nothing was charged. | Try again later. |
PAID_SUBSERVER_LIMIT_REACHED | Paid subserver limit reached | The account already has as many paid subservers as its limit allows. data.max is the limit and data.used the paid subservers counted toward it. Nothing was charged. | Contact support to raise the limit. GET /api/v1/users/me shows the limit and the count under limits.paid_subservers. |
SUBSERVER_DORMANT | Subserver is not active | The container is on a paid subserver whose subscription is not active (on hold, being delivered, or being deleted) or has a change in progress, so containers on it cannot be created, started, stopped, changed, copied, shared or deleted. data.reason is not_active, operation_in_flight or no_subscription. Nothing was changed. | Retry once the subserver is active again (renew a held subscription, or wait for the change to finish). |
IDEMPOTENCY_INTENT_MISMATCH | Idempotency key reused for a different purchase | The supplied idempotency_key was already used for a reservation with different terms (a different offer, duration, pool or confirmed total). Returning the original would silently give the caller something other than what this request asked for, so it fails instead. | Use a fresh idempotency_key for a genuinely new purchase. |
CHARGE_CONFIRMATION_REQUIRED | The total must be confirmed before money moves | The request would debit an account and carried no max_charge_cents, so there was nothing to bind the quoted price to the charged one. Nothing was charged. This applies to every paid purchase, including servers with no one-time setup fee. | Read the current total, show it to the customer, and retry with max_charge_cents set to the value they confirmed. Never auto-retry with the total from this error: that defeats the confirmation it exists to obtain. |
CHARGE_EXCEEDS_MAX | Price changed since the quote | The live total (rental price plus any one-time setup fee) is higher than the max_charge_cents ceiling supplied with the request, so nothing was charged. | Re-read the listing, show the user the new total, and retry with an updated max_charge_cents once they confirm. Do NOT auto-retry with the returned actual value. |
Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}