Developers

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.

Bearer tokens with scopes600 requests / minuteJSON in, JSON outOptional IP allow-list
# base URL https://joy.services/api/v1 # every request curl -H "Authorization: Bearer $TOKEN" https://joy.services/api/v1/me

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.

MethodEndpointScopePurpose
GET/mereadAccount profile, wallet balance and the scopes of the calling token
GET/locationsreadRegions with live availability, IPv4 stock, price multiplier and installable OS codes
GET/plansreadFixed plans (USD), component prices for custom builds and billing cycles
GET/osreadOS catalogue (code, name, family)
POST/quotereadPrice a plan or custom spec without ordering
GET/instancesreadList your servers with live hypervisor status
POST/instancesorders.writeOrder + deploy a server; pays from the wallet by default
GET/instances/{id}readFull detail incl. IPs, credentials, billing and last job
GET/instances/{id}/statsreadLive snapshot plus a recent time-series (CPU, network, RAM)
POST/instances/{id}/{action}instances.writestart · stop · shutdown · reboot · reset · reinstall · password · snapshot · rename
DELETE/instances/{id}instances.writeTerminate the server and release its IPs (requires confirm)
GET/jobs/{id}readPoll an asynchronous job
GET/billingbilling.readWallet, currency, unpaid invoices, monthly run-rate
GET/invoicesbilling.readLast 100 invoices with pay links
GET/ticketsreadYour support tickets
POST/ticketstickets.writeOpen a ticket
GET/statusreadPlatform status plus the health of your own instances
GET/dns/zonesreadYour DNS zones, our name servers and the price of one more zone
POST/dns/zonesdns.writeAdd a zone ({"domain": "example.com"}); created on both name servers at once
GET/dns/zones/{zone}readA zone with every record
DELETE/dns/zones/{zone}dns.writeRemove a zone and its records (requires "confirm": the domain)
POST/dns/zones/{zone}/checkreadAsk the registry (the parent zone's servers) which name servers the domain is delegated to — active when it is ours
GET/dns/typesreadEvery record type our name servers serve, with what its content is and an example
POST/dns/zones/{zone}/recordsdns.writeAdd a record: type, name (@ = root), content, ttl, priority (MX/SRV)
PUT/dns/zones/{zone}/records/{record}dns.writeChange a record (any of type, name, content, ttl, priority, disabled)
DELETE/dns/zones/{zone}/records/{record}dns.writeDelete 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.

ScopeGrants
readAll GET endpoints, POST /quote
orders.writePOST /instances — creating orders and paying them from the wallet
instances.writePower actions, reinstall, password reset, snapshots, rename, DELETE /instances/{id}
billing.readGET /billing, GET /invoices
tickets.writePOST /tickets
dns.writeAdding / 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).

# 401 — missing or invalid token { "ok": false, "error": "Invalid token", "status": 401 } # 403 — token exists but lacks the scope { "ok": false, "error": "Token lacks scope: instances.write", "status": 403 }

Errors, limits & conventions

  • Every response has ok: true|false. Failures also carry error (human-readable) and status (the HTTP code).
  • Rate limit: 600 requests per minute per token → 429 with Retry-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 return 404. An order that could not be paid from the wallet returns 402 with a pay_url.
  • Money is always USD as a float (total_usd); where a local amount is included it is labelled (total_local, currency). Timestamps are UTC YYYY-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?

{ "ok": true, "account": { "id": "9f2c…", "email": "ops@example.com", "username": "example", "name": "Example Ltd", "currency": "INR", "wallet_usd": 42.5, "created_at": "2025-11-02 09:14:00" }, "token": { "name": "ci-runner", "scopes": ["read", "orders.write", "instances.write"] } }

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.

{ "ok": true, "locations": [ { "code": "mumbai", "name": "Mumbai, India", "country": "IN", "region": "asia-south", "available": true, "ipv4_stock": 29, "price_multiplier": 1.0, "os": ["ubuntu-22", "ubuntu-24", "win-2022", "win-2025"] }, … ] }

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:

FieldTypeNotes
locationstringLocation code from /locations (required)
osstringOS code, e.g. ubuntu-24, win-2022 (required)
planstringPlan code — omit when sending custom
customobject{"cores":4,"ram_gb":8,"disk_gb":120} within the component ranges
cyclestringhourly · monthly (default) · quarterly · semiannual · annual
extra_ipv4int0–8 additional addresses
couponstringoptional 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_gbintOff-node backup space in 10 GB steps
port_10gboolUpgrade to a 10 Gbps port
curl -X POST https://joy.services/api/v1/quote -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"location":"mumbai","os":"ubuntu-24","plan":"small","cycle":"annual","extra_ipv4":1}' { "ok": true, "quote": { "total_usd": 112.1, "monthly_usd": 9.34, "hourly_usd": 0.0135, "months": 12, "currency": "INR", "total_local": 9412.44, "lines": [ {"label":"Small — 2 vCPU / 4 GB / 60 GB NVMe","usd":8.99}, {"label":"Extra IPv4 × 1","usd":2.0}, {"label":"Annual discount 15%","usd":-1.65} ], "spec": { "cores": 2, "ram_mb": 4096, "disk_gb": 60 } } }

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.

{ "id": "498b5959b4257a8671edd51f2b5cebf7", "hostname": "api-01", "ip": "138.252.100.24", "location": "mumbai", "os": "ubuntu-24", "status": "running", "spec": { "cores": 2, "ram_gb": 4, "disk_gb": 60, "bandwidth_tb": 3 }, "billing": { "cycle": "monthly", "price_usd_month": 8.99, "next_due": "2026-10-09 00:00:00", "auto_renew": true }, "live": { "cpu_pct": 3.2, "mem_bytes": 1287651328, "mem_max": 4294967296, "net_in_bps": 18400, "net_out_bps": 9120, "uptime": 86400, "hypervisor_status": "running", "stale": false }, "created_at": "2026-09-01 10:22:13", "tags": null }

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.

# deploy from wallet balance curl -X POST https://joy.services/api/v1/instances -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"location":"mumbai","os":"ubuntu-24","plan":"small","cycle":"hourly","hostname":"api-01","ssh_key":"ssh-ed25519 AAAA…","pay":"wallet"}' # 201 Created { "ok": true, "order": "ORD-260909-3F1A2", "instance": "9f2c…", "job": 1842, "invoice": { "id": 5120, "no": "JS-2609-AB12C4", "status": "paid", "total_usd": 0.0135, "pay_url": "https://joy.services/dash/billing/invoice/5120" } } # 402 Payment Required — wallet too low; pay the invoice, deployment starts automatically { "ok": true, "order": "ORD-260909-3F1A2", "invoice": { "id": 5120, "no": "JS-2609-AB12C4", "status": "unpaid", "pay_url": "…" }, "error": "Insufficient wallet balance" }

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.

ActionBodyEffect
start · stop · shutdown · reboot · resetPower 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.
passwordGenerates 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.
curl -X POST https://joy.services/api/v1/instances/498b59…/reinstall -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"os":"win-2022"}' { "ok": true, "job": 1901, "poll": "https://joy.services/api/v1/jobs/1901" } # unknown action { "ok": false, "error": "Unknown action", "status": 404 }

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 (queuedrunningdone | 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.

{ "ok": true, "job": { "id": 1842, "status": "running", "step": "agent", "progress": 70, "type": "deploy", "instance": "9f2c…", "created_at": "2026-09-09 10:00:02", "finished_at": null } }

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.

{ "ok": true, "wallet_usd": 42.5, "currency": "INR", "unpaid": [ { "id": 5133, "no": "JS-2609-C0FFEE", "total": 771.48, "currency": "INR", "total_usd": 8.99, "due": "2026-09-16 00:00:00", "pay_url": "…" } ], "monthly_run_rate_usd": 25.98 }

Tickets

GET/tickets lists your tickets (id, ref, subject, status, priority, department, url). POST/tickets (scope tickets.write, 5 per hour) opens one:

curl -X POST https://joy.services/api/v1/tickets -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"subject":"rDNS for 138.252.100.24","message":"Please set PTR to mail.example.com","priority":"normal","instance":"498b59…"}'

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.

# add a zone, then an A record at the root and a www CNAME curl -X POST https://joy.services/api/v1/dns/zones -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"domain":"example.com"}' curl -X POST https://joy.services/api/v1/dns/zones/$ZONE/records -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"type":"A","name":"@","content":"203.0.113.10","ttl":300}' curl -X POST https://joy.services/api/v1/dns/zones/$ZONE/records -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"type":"CNAME","name":"www","content":"example.com"}' # change the TTL of a record, then remove it curl -X PUT https://joy.services/api/v1/dns/zones/$ZONE/records/$RECORD -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"ttl":60}' curl -X DELETE https://joy.services/api/v1/dns/zones/$ZONE/records/$RECORD -H "Authorization: Bearer $TOKEN" # is the domain pointing at us yet? curl -X POST https://joy.services/api/v1/dns/zones/$ZONE/check -H "Authorization: Bearer $TOKEN"

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.

#!/usr/bin/env bash — requires curl + jq API=https://joy.services/api/v1; H=(-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json") # 1. is Mumbai accepting deploys? curl -s "${H[@]}" $API/locations | jq '.locations[] | select(.code=="mumbai") | {available, ipv4_stock}' # 2. deploy (hourly, paid from wallet) JOB=$(curl -s "${H[@]}" -X POST $API/instances \ -d '{"location":"mumbai","os":"ubuntu-24","plan":"nano","cycle":"hourly","hostname":"ci-01","pay":"wallet"}' | jq -r .job) ID=$(curl -s "${H[@]}" $API/instances | jq -r '.instances[] | select(.hostname=="ci-01") | .id') # 3. wait until the job is done until [ "$(curl -s "${H[@]}" $API/jobs/$JOB | jq -r .job.status)" = done ]; do sleep 3; done # 4. credentials curl -s "${H[@]}" $API/instances/$ID | jq '{ip: .instance.ip, user: .instance.credentials.user, password: .instance.credentials.password}' # 5. tear down curl -s "${H[@]}" -X DELETE $API/instances/$ID -d '{"confirm":"ci-01"}'

Legacy Instances API (/api/connect)

Legacy — maintained for existing integrations only. New projects should use REST API v1 above. The legacy endpoint keeps working for customers who received an 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.

HeaderRequirementPurpose
AcceptRecommendedEnsures a JSON response (application/json)
Content-TypeRequiredRequests must be JSON
User-AgentRecommendedUsed for logging / telemetry (joyconnect)
x-api-tokenRequiredPrimary token identifier, e.g. JOY-API-TOKEN#API
x-api-keyRequiredKey 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": "dashboardinfo", "init_ip": "127.0.0.1", "init_browser": "Chrome" } { "success": "true", "amount": "932.91", "instances": 18, "iram": 115, "icore": 78, "hostings": 33, "databases": 14, "dns_domains": 15, "cdn_domains": 1, "total": 81 }

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.

{ "success": "true", "value": [ { "id": 342, "machine": "6001", "machine_node": "c15-node-joy-london", "ip": "10.10.112.65", "platform": "ubuntu", "price": null, "extime": "33285031929", "expire": null, "ram": 4, "core": 2, "storage": 48, "ihash": "498b5959b4257a8671edd51f2b5cebf7" } ] }

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": "machine_info", "init_ip": "127.0.0.1", "init_browser": "Chrome", "instance_token": "498b5959b4257a8671edd51f2b5cebf7" } { "success": true, "value": "498b5959b4257a8671edd51f2b5cebf7", "ip": "10.10.240.21", "os": "ubuntu", "extime": "33285031929", "Current": { "name": "user-x-25nov", "status": "running", "vmid": 6001, "cpus": 2, "cpu": 0.015, "mem": 3405574144, "maxmem": 4294967296, "freemem": 696369152, "uptime": 29461293, "netin": 2476753613887, "netout": 18566030361, "diskread": 173319141376, "diskwrite": 788248155136, "maxdisk": 12884901888, "nics": { "tap6001i0": { "netin": 2476753613887, "netout": 18566030361 } } }, "qemuConfig": { "cores": 1, "sockets": 2, "memory": "4096", "net0": "virtio=BC:24:11:E1:C1:54,bridge=vmbr0,firewall=1", "scsi0": "ssd-raid:6001/vm-6001-disk-0.raw,…,size=12G,ssd=1", "onboot": 1, "agent": "1" }, "osinfo": null }

type = change_machine_state

Changes the power state with machine_state; unsupported values return {"success": false, "error": "invalid_state"}.

StateActionSuccess value
startPower onmachine_already_started
shutdownGraceful shutdownshutdown_success
force_shutdownHard stopstopped_successfully
rebootGraceful rebootreboot_success
force_rebootHard resetpower_reset_success
pauseSuspendmachine_paused
hibernateHibernatemachine_hibernated
resumeResumemachine_already_resumed
curl -X POST "https://joy.services/api/connect/" \ -H "Accept: application/json" -H "Content-Type: application/json" -H "User-Agent: joyconnect" \ -H "x-api-token: JOY-API-xxxxxxxxxxxx" -H "x-api-key: joy-xxxxxxxxxxxxxxxx" \ --data-raw '{ "type": "change_machine_state", "machine_state": "reboot", "instance_token": "cf25998c59fdbf37f0fada2517242f47", "init_ip": "127.0.0.1", "init_browser": "Chrome" }' { "success": true, "value": "reboot_success" }

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.

{ "success": "false", "error": "invalid_api_credentials", "message": "Token or key is invalid or disabled." }

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?
Not a separate one — use hourly billing. A Nano server costs a fraction of a cent per hour, so an end-to-end test (deploy, poll, destroy) costs less than a cent and exercises the real pipeline.
Are there webhooks or a Terraform provider?
Not yet. Poll /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_listGET /instances (the ihash is the same id); machine_infoGET /instances/{id} and /stats; change_machine_statePOST /instances/{id}/start|stop|shutdown|reboot|reset; dashboardinfo / account_infoGET /me and GET /billing.
Can I restrict a token to one server?
Tokens are account-wide but scope-limited. For automation that should only reboot a specific box, create a token with instances.write only and an IP allow-list for the host that runs the script.
Changelog
v1.0 — initial public release: catalogue, quotes, instances, actions, jobs, billing, invoices, tickets, status. Legacy /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.