Joy REST API v1
Everything in the console is available over HTTPS with a Bearer token: quotes, deployments, power actions, reinstalls, snapshots, live stats, invoices and tickets. Responses are JSON, endpoints are versioned and additive — v1 will not break under you.
Overview
The API mirrors the console: what you can click, you can call. All endpoints live under https://joy.services/api/v1, accept and return application/json, and are authenticated with a Bearer token that you create in the console. Deployments are asynchronous — you receive a job id and poll it — everything else answers immediately.
| Method | Endpoint | Scope | Purpose |
|---|---|---|---|
| GET | /me | read | Account profile, wallet balance and the scopes of the calling token |
| GET | /locations | read | Regions with live availability, IPv4 stock, price multiplier and installable OS codes |
| GET | /plans | read | Fixed plans (USD), component prices for custom builds and billing cycles |
| GET | /os | read | OS catalogue (code, name, family) |
| POST | /quote | read | Price a plan or custom spec without ordering |
| GET | /instances | read | List your servers with live hypervisor status |
| POST | /instances | orders.write | Order + deploy a server; pays from the wallet by default |
| GET | /instances/{id} | read | Full detail incl. IPs, credentials, billing and last job |
| GET | /instances/{id}/stats | read | Live snapshot plus a recent time-series (CPU, network, RAM) |
| POST | /instances/{id}/{action} | instances.write | start · stop · shutdown · reboot · reset · reinstall · password · snapshot · rename |
| DELETE | /instances/{id} | instances.write | Terminate the server and release its IPs (requires confirm) |
| GET | /jobs/{id} | read | Poll an asynchronous job |
| GET | /billing | billing.read | Wallet, currency, unpaid invoices, monthly run-rate |
| GET | /invoices | billing.read | Last 100 invoices with pay links |
| GET | /tickets | read | Your support tickets |
| POST | /tickets | tickets.write | Open a ticket |
| GET | /status | read | Platform status plus the health of your own instances |
| GET | /dns/zones | read | Your DNS zones, our name servers and the price of one more zone |
| POST | /dns/zones | dns.write | Add a zone ({"domain": "example.com"}); created on both name servers at once |
| GET | /dns/zones/{zone} | read | A zone with every record |
| DELETE | /dns/zones/{zone} | dns.write | Remove a zone and its records (requires "confirm": the domain) |
| POST | /dns/zones/{zone}/check | read | Ask the registry (the parent zone's servers) which name servers the domain is delegated to — active when it is ours |
| GET | /dns/types | read | Every record type our name servers serve, with what its content is and an example |
| POST | /dns/zones/{zone}/records | dns.write | Add a record: type, name (@ = root), content, ttl, priority (MX/SRV) |
| PUT | /dns/zones/{zone}/records/{record} | dns.write | Change a record (any of type, name, content, ttl, priority, disabled) |
| DELETE | /dns/zones/{zone}/records/{record} | dns.write | Delete a record |
Unknown paths under /api/ return 404 {"ok":false,"error":"Unknown endpoint — see /api-integration"}.
Authentication & scopes
Send the token in the Authorization header. Tokens are 20–200 characters, start with joy_ and are stored hashed — if you lose one, revoke it and create another. Each token carries a comma-separated list of scopes; a token with the * scope can do everything. Any token with at least one scope satisfies read.
| Scope | Grants |
|---|---|
read | All GET endpoints, POST /quote |
orders.write | POST /instances — creating orders and paying them from the wallet |
instances.write | Power actions, reinstall, password reset, snapshots, rename, DELETE /instances/{id} |
billing.read | GET /billing, GET /invoices |
tickets.write | POST /tickets |
dns.write | Adding / removing DNS zones and records (/dns/…) |
A token can also be limited to an IP allow-list (CIDR list) and given an expiry date. Requests from other addresses get 403 IP not allowed for this token; expired tokens get 401 Token expired. API access can be disabled for a whole account from the console (403 API disabled for this account).
Errors, limits & conventions
- Every response has
ok: true|false. Failures also carryerror(human-readable) andstatus(the HTTP code). - Rate limit: 600 requests per minute per token →
429withRetry-After. Deployments are limited to 10 per hour per account, ticket creation to 5 per hour, power actions to 10 per minute per instance and password resets to 3 per 10 minutes. - Validation errors return
422. Missing resources return404. An order that could not be paid from the wallet returns402with apay_url. - Money is always USD as a float (
total_usd); where a local amount is included it is labelled (total_local,currency). Timestamps are UTCYYYY-MM-DD HH:MM:SS. - Instance ids are opaque 32-character hashes (
ihash), never the hypervisor VM id. - Every call is logged (method, path, status, IP, duration) and visible under Console → Account → API tokens.
Account
GET/me — who am I, and what can this token do?
Catalogue & quotes
GET/locations returns regions with available (true when a node is online, a template is verified and IPv4 is in stock), ipv4_stock, price_multiplier and the OS codes installable there.
GET/plans returns three arrays: plans (code, name, family, cores, ram_mb, disk_gb, bandwidth_tb, price_usd_month, price_usd_hour, windows_addon_usd), components keyed by component (unit_price_usd, unit, min, max, step — for custom builds) and cycles (code, months, discount_pct). GET/os lists operating systems.
POST/quote prices a spec without ordering. Send either plan or custom:
| Field | Type | Notes |
|---|---|---|
location | string | Location code from /locations (required) |
os | string | OS code, e.g. ubuntu-24, win-2022 (required) |
plan | string | Plan code — omit when sending custom |
custom | object | {"cores":4,"ram_gb":8,"disk_gb":120} within the component ranges |
cycle | string | hourly · monthly (default) · quarterly · semiannual · annual |
extra_ipv4 | int | 0–8 additional addresses |
coupon | string | optional discount code — the quote returns coupon (code, label, discount_usd) when it applies, or coupon_error saying why not; an order with a code that does not apply is refused with that message |
backup_gb | int | Off-node backup space in 10 GB steps |
port_10g | bool | Upgrade to a 10 Gbps port |
Instances
GET/instances lists your servers. Each instance object has the shape below; live is null when the hypervisor has not reported yet, and stale is true when the last report is older than a minute.
POST/instances takes the same body as /quote plus hostname (required, DNS label), optional ssh_key (public key, Linux only) and pay (wallet — default — pays the invoice immediately from your balance; anything else leaves it unpaid with a pay_url). Deployment starts the moment the invoice is paid. Rate limit: 10 deployments per hour.
GET/instances/{id} adds ips[] (ip, gateway, netmask, rdns), credentials (user = root or Administrator, password) and last_job. Treat this response as secret. GET/instances/{id}/stats returns the live snapshot plus series[] of {ts, cpu_pct, net_in_bps, net_out_bps, mem_bytes} points collected every few seconds for the last ~15 minutes.
Power, reinstall, snapshots
POST/instances/{id}/{action} — all actions except rename are asynchronous and answer 202 with a job id and a poll URL.
| Action | Body | Effect |
|---|---|---|
start · stop · shutdown · reboot · reset | — | Power on, hard stop, ACPI shutdown, ACPI reboot, hard reset. 10 per minute per instance. |
reinstall | {"os":"ubuntu-22","hostname":"optional"} | Wipes the disk, keeps the IP and VMID, installs the template and generates a new password. |
password | — | Generates and applies a new root / Administrator password through the guest agent. 3 per 10 minutes. |
snapshot | {"op":"create|rollback|delete","name":"pre-upgrade"} | NVMe snapshots on the node. Free; not a backup. |
rename | {"hostname":"web-02"} | Synchronous. Lower-case DNS label, 1–63 chars. |
DELETE/instances/{id} terminates the server and releases its IPs. To prevent accidents the body must repeat the hostname: {"confirm":"api-01"}; otherwise 422 Send {"confirm": "<hostname>"} to destroy. Hourly billing stops immediately; prepaid terms are not refunded.
Jobs
Every asynchronous action returns a job id. GET/jobs/{id} returns status (queued → running → done | failed), step, progress (0–100), type, instance, created_at and finished_at. Deployment steps are clone → configure → boot → agent → network → ready. Poll every 2–5 seconds; a typical deployment finishes in 60–120 seconds.
Billing & invoices
GET/billing (scope billing.read) returns wallet_usd, currency, unpaid[] (id, no, total, currency, total_usd, due, pay_url) and monthly_run_rate_usd — the sum of all active services' monthly prices. GET/invoices returns the last 100 invoices with kind, status, totals, dates and a console URL. Invoices cannot be paid through the API; use the pay_url or top up the wallet.
Tickets
GET/tickets lists your tickets (id, ref, subject, status, priority, department, url). POST/tickets (scope tickets.write, 5 per hour) opens one:
Status
GET https://joy.services/status.json is public, needs no token and is cached for 30 seconds — use it for dashboards and monitors. GET/status on the API returns the same components and incidents plus my_instances[] with each server's status and live hypervisor state.
DNS
Zones on our name servers, the same ones the console manages. GET/dns/zones lists yours (with nameservers[] to set at your registrar and price_usd_month for one more zone). POST/dns/zones (scope dns.write) adds one — a domain or a reverse zone such as 16.7.201.in-addr.arpa; it exists on both name servers when the call returns. GET/dns/zones/{zone} returns the zone with its records[] (id, type, name — @ is the root —, fqdn, content, ttl, priority, disabled). Records: POST/dns/zones/{zone}/records, PUT and DELETE on /dns/zones/{zone}/records/{record}. Types: every type our name servers serve — A, AAAA, CNAME, ALIAS, MX, TXT, SPF, SRV, CAA, NS, PTR, DNAME, TLSA, SMIMEA, SSHFP, OPENPGPKEY, IPSECKEY, CERT, DHCID, HTTPS, SVCB, URI, NAPTR, HINFO, RP, LOC, KX, AFSDB, MINFO, MR, the DNSSEC set (DS, CDS, DNSKEY, CDNSKEY, KEY, RKEY, DLV, CSYNC, ZONEMD, NSEC, NSEC3, NSEC3PARAM, RRSIG, SIG, TKEY, TSIG), A6, APL, WKS, EUI48, EUI64, L32, L64, LP, NID, MAILA, MAILB and LUA; GET/dns/types lists them with the content each expects. Host names in content may be given with or without the final dot; MX/SRV take priority (SRV content is weight port target); TTL 60–604800. A validation problem is a 422 with the reason (the name server's own reason when it refuses a rare type); a name-server problem is a 502 and nothing was changed.
Every zone carries state: pending until the domain is delegated to our name servers, then active (suspended for an unpaid renewal). We ask the registry every 30 minutes and flip the state ourselves; POST/dns/zones/{zone}/check asks at once and returns found[] (the delegation at the parent — source "parent" —, or the public resolvers' view when the parent did not answer), missing[] (ours not yet set), extra[] (foreign name servers still listed) and exact.
Examples — curl · PHP · JavaScript
A complete flow: check the region, quote, deploy on hourly billing, poll the job, print the IP and password, and destroy the server at the end.
Legacy Instances API (/api/connect)
x-api-token / x-api-key pair; it is not extended with new features.The legacy API is a single JSON POST endpoint at https://joy.services/api/connect/. Every request selects an operation with the type field and is authenticated with two headers. Treat both values as secrets: do not expose them in public repositories, client-side apps or browser code.
| Header | Requirement | Purpose |
|---|---|---|
Accept | Recommended | Ensures a JSON response (application/json) |
Content-Type | Required | Requests must be JSON |
User-Agent | Recommended | Used for logging / telemetry (joyconnect) |
x-api-token | Required | Primary token identifier, e.g. JOY-API-TOKEN#API |
x-api-key | Required | Key associated with the token, e.g. joy-xxx… |
Body fields: type (required — dashboardinfo, account_info, machine_list, machine_info, change_machine_state), init_ip and init_browser (recommended — the client IP and browser/app identifier captured by your app).
type = dashboardinfo
Fetches dashboard totals: amount (wallet, string), instances, iram (total RAM), icore (total vCPU), hostings, databases, dns_domains, cdn_domains, total. Note that success is returned as the string "true"/"false" in this format; treat numeric fields as integers.
type = account_info
Returns the account profile: email, verify (1 = verified), twofa (1 = enabled), username, full_name, gender, profile_pic, ip, time, and an api_token object (api_token, api_key, api_reg_ip, status enabled/disabled).
type = machine_list
Lists active compute instances in value[]: id, machine (short id), machine_node (node label), ip, platform (ubuntu / windows), ram, core, storage, expire, extime, price and ihash. Use the ihash as instance_token for the calls below.
type = machine_info
Real-time VM details for one instance (instance_token required). Current holds runtime metrics — status, vmid, cpu (fractional; multiply by 100 for %), mem / maxmem / freemem (bytes), uptime (s), netin / netout, diskread / diskwrite, maxdisk (bytes), nics per interface — and qemuConfig holds the VM configuration (cores, memory, net0, scsi0, …). osinfo may be null. Some values arrive as strings depending on serialization; coerce types in your client.
type = change_machine_state
Changes the power state with machine_state; unsupported values return {"success": false, "error": "invalid_state"}.
| State | Action | Success value |
|---|---|---|
start | Power on | machine_already_started |
shutdown | Graceful shutdown | shutdown_success |
force_shutdown | Hard stop | stopped_successfully |
reboot | Graceful reboot | reboot_success |
force_reboot | Hard reset | power_reset_success |
pause | Suspend | machine_paused |
hibernate | Hibernate | machine_hibernated |
resume | Resume | machine_already_resumed |
Errors. Typical failure causes are missing or invalid headers, an invalid JSON body, an unsupported type, or a request blocked by server-side rules. Check the HTTP status code and parse JSON safely; your backend may return JSON as an encoded string in some layers, so if the first parse returns a string, parse again.
Security notes. If you are building a public website or a client-side SPA, route requests through your backend and keep tokens server-side. Use HTTPS, rotate keys if compromised, restrict allowed IPs where supported, and log request IDs for support.
FAQ & changelog
Is there a sandbox?
Are there webhooks or a Terraform provider?
/jobs/{id} for now; webhooks for job completion and invoice events are on the roadmap and will be announced in the Updates feed. Tell us in a ticket what you need — the API is versioned and additive.How do I migrate from the legacy API?
machine_list → GET /instances (the ihash is the same id); machine_info → GET /instances/{id} and /stats; change_machine_state → POST /instances/{id}/start|stop|shutdown|reboot|reset; dashboardinfo / account_info → GET /me and GET /billing.Can I restrict a token to one server?
instances.write only and an IP allow-list for the host that runs the script.Changelog
/api/connect remains available for existing token/key pairs.Need something the API does not expose yet? Open a ticket to the Developers department — we read every one.