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 http://127.0.0.1/api/v1 # every request curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1/api/v1/me

Overview

The API mirrors the console: what you can click, you can call. All endpoints live under http://127.0.0.1/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

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

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
backup_gbintOff-node backup space in 10 GB steps
port_10gboolUpgrade to a 10 Gbps port
curl -X POST http://127.0.0.1/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 http://127.0.0.1/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": "http://127.0.0.1/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 http://127.0.0.1/api/v1/instances/498b59…/reinstall -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"os":"win-2022"}' { "ok": true, "job": 1901, "poll": "http://127.0.0.1/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 http://127.0.0.1/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 http://127.0.0.1/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.

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=http://127.0.0.1/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://api.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://api.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.