Skip to content

Developers

Nextify API

Everything you do in the portal, from scripts, CI pipelines and Terraform: instances, storage, networking, load balancers, VPN and billing. JSON over HTTPS, authenticated with an API token you create yourself.

Quick start

Create a token in the portal under Account → API tokens and keep it in your environment or your CI's secret store. The examples use curl and jq.

export NEXTIFY_TOKEN=nxt_...        # Portal → Account → API tokens
API=https://api.nextifynow.com/v1
nx() { curl -sS -H "Authorization: Bearer $NEXTIFY_TOKEN" -H "Content-Type: application/json" "$@"; }

nx $API/me | jq                        # who am I

# Follow a background job to its end, printing its progress; fails if the job fails.
wait_job() {
  while :; do
    J=$(nx $API/jobs/$1)
    echo "$J" | jq -r '"\(.progress.percent)%  \(.progress.stage)"'
    case $(echo "$J" | jq -r .status) in
      succeeded) return 0 ;;
      failed) echo "$J" | jq -r '.error.message' >&2; return 1 ;;
    esac
    sleep 3
  done
}

Pick a machine type and an image for the region. These two routes are public:

curl -s "$API/machines?region=us-west-1" \
  | jq -r '.items[] | "\(.code)\t\(.vcpu) vCPU\t\(.ram_gb) GB\t\(.price_month_cents/100) USD/month"'
curl -s "$API/images?region=us-west-1&machine_type=sm1.s1-1" | jq -r '.items[].slug'

Add your SSH key once, create an instance with it, and follow the create to the end. Every asynchronous call answers with a job_id; the wait_job helper above prints its progress and fails if the job does:

KEY_ID=$(jq -n --arg k "$(cat ~/.ssh/id_ed25519.pub)" '{name: "laptop", public_key: $k}' \
  | nx -X POST $API/ssh-keys -d @- | jq -r .id)

NEW=$(jq -n --arg key "$KEY_ID" \
  '{name: "web-1", machine_type: "sm1.s1-1", region: "us-west-1", image: "ubuntu-24.04", ssh_key_ids: [$key]}' \
  | nx -X POST $API/instances -d @-)
ID=$(echo "$NEW" | jq -r .id)
wait_job "$(echo "$NEW" | jq -r .job_id)"
IP=$(nx $API/instances/$ID | jq -r .public_ip)
ssh ubuntu@$IP

When you are done, delete it. Billing stops when the instance is deleted.

wait_job "$(nx -X DELETE $API/instances/$ID | jq -r .job_id)"

Authentication

Send the token on every request:

Authorization: Bearer nxt_...
  • Full access tokens can do everything your account's users can do in the API.
  • Read-only tokens can make GET requests only; anything else answers 403 read_only_token. Use them for monitoring and inventory.
  • A token can have an expiry date or none. An expired token answers 401 token_expired; a revoked or unknown one 401 unauthorized. Revoking takes effect on the next request.
  • Tokens are shown once, when you create them. Nextify keeps only a fingerprint. Tokens cannot create, list or revoke tokens: that needs a signed-in user in the portal.
  • The catalog routes (/v1/regions, /v1/machines, /v1/pricing, /v1/images) need no token.

Requests and responses

  • Base URL https://api.nextifynow.com/v1. Paths are exact: a trailing slash or an unsupported method answers 404.
  • Send JSON bodies with Content-Type: application/json.
  • Lists return {"items": [...]} with every row; there is no paging. A single resource is the object itself. A few creates wrap the new object in a named key (for example {"vpc": {...}}); each endpoint below shows its response.
  • Ids are UUIDs. Money is in US cents and may be fractional (price_hour_cents: 0.86). Sizes are bytes in responses and GB where a request says so.
  • Timestamps are ISO 8601 in UTC (2026-10-18T09:30:00Z).
  • GET /v1/instances lists live instances. Add ?status=running,stopped to filter, or ?include_deleted=true to see deleted ones too; GET /v1/instances/{id} still answers a deleted instance with status: "deleted".
  • Instance and volume names are unique among your account's live resources: a create with a name in use answers 409 name_taken, and a deleted resource's name is free again. Bucket names are global across all accounts.

Errors

Errors carry an HTTP status and a body with a stable code and a readable message. The one exception is a quota refusal, shown under Rate limits and quotas.

HTTP/1.1 400 Bad Request
{"error": {"code": "bad_request", "message": "name must be 1-64 printable characters"}}
StatusMeaning
400The request is invalid; the message names the field.
401No token, or an unknown, revoked or expired one.
402payment_method_required: add a card in the portal's Billing page before renting (creating, growing or powering on). The card is charged only for what you use.
403The token may not do this (read-only token, token management).
404No such resource on your account, or no such route.
409Conflict with the current state, or a quota (below).
429Rate limited; wait for Retry-After seconds.
5xxOur side. 503 carries Retry-After when the service is briefly unavailable.

Background jobs and progress

Creating, changing and deleting instances, volumes, snapshots, buckets, public IPs, private networks, firewalls, VPN gateways and load balancers happens in the background. The request returns at once (usually 202) with the resource in a transitional status and a job_id. Follow the job with GET /v1/jobs/{job_id}; every few seconds is enough:

GET /v1/jobs/48213
{"id": "48213", "type": "snapshot.create", "action": "Create snapshot", "status": "running",
 "progress": {"percent": 20, "stage": "Taking the snapshot and copying it to durable storage"},
 "resource": {"type": "snapshot", "id": "6f0c2f4e-…"}, "attempts": 0, "error": null,
 "created_at": "2026-10-18T21:04:05Z", "started_at": "2026-10-18T21:04:06Z", "finished_at": null}
statusMeaning
queuedAccepted, waiting for a worker.
runningIn progress. progress.percent and progress.stage say how far it is; the percent moves in steps as each stage finishes.
retryingAn attempt failed and the job tries again at next_attempt_at; error says what went wrong.
succeededDone (percent 100).
failedFinal. error.code and error.message say why and what to do.

GET /v1/jobs?resource_id={id} lists the jobs of one resource, newest first, and ?status=failed finds what went wrong. A snapshot also carries its latest create, restore or delete as last_job, so one read shows how far a snapshot or a restore has got:

# Snapshot an instance and wait for it
SNAP=$(nx -X POST $API/instances/$ID/snapshots -d '{"name": "before-upgrade"}')
SNAP_ID=$(echo "$SNAP" | jq -r .snapshot.id)
wait_job "$(echo "$SNAP" | jq -r .job_id)"

# Progress at a glance, from the snapshot itself
nx $API/snapshots/$SNAP_ID | jq '{status, job: .last_job.status, progress: .last_job.progress}'

# Roll back: stop the instance, restore, start it again
wait_job "$(nx -X PATCH $API/instances/$ID -d '{"desired_status": "stopped"}' | jq -r .job_id)"
wait_job "$(nx -X POST $API/snapshots/$SNAP_ID/restore | jq -r .job_id)"
wait_job "$(nx -X PATCH $API/instances/$ID -d '{"desired_status": "running"}' | jq -r .job_id)"

You can also poll the resource itself until it settles:

OperationPollDone when
Create, start or stop an instanceGET /v1/instances/{id}status equals desired_status. error means it failed; after a failed start or stop, power_error says why.
Delete an instanceGET /v1/instances/{id}status is deleted
Create, attach or detach a volumeGET /v1/volumes/{id}available or attached
Create a snapshotGET /v1/snapshots/{id}ready (or error)
Create a bucketGET /v1/buckets/{id}active (or error)

Snapshot schedules

Let Nextify take an instance's snapshots for you. POST /v1/instances/{id}/snapshot-schedules adds a schedule: hourly, daily, weekly, monthly or yearly, at a time in UTC, with its own retention. An instance can have up to 10, so one daily schedule kept 14 weeks, one monthly kept 12 months and one yearly kept 7 years together give you long-term backups.

  • Retention: keep_count keeps the newest N, retain ({"value": 14, "unit": "weeks"}) deletes snapshots older than that. Give one or both; with both, whichever deletes first.
  • A schedule deletes only the snapshots it took. Manual snapshots and other schedules' snapshots are never touched. Each snapshot carries schedule_id, so GET /v1/snapshots shows which schedule took it.
  • Scheduled snapshots are billed like manual ones (per GB-month) and count against the same snapshot quotas. When a run cannot take one (a quota, no card, the instance busy), last_error says why and the next run tries again.
  • PATCH /v1/snapshot-schedules/{id} with "enabled": false pauses it; old snapshots are still deleted by its retention. DELETE /v1/snapshot-schedules/{id} stops it and keeps its snapshots as manual ones; add ?delete_snapshots=true to delete them too.
# Daily kept 14 weeks, monthly kept 12 months, yearly kept 7 years (times in UTC)
for BODY in \
  '{"name": "daily",   "frequency": "daily",   "hour": 3, "minute": 0, "retain": {"value": 14, "unit": "weeks"}}' \
  '{"name": "monthly", "frequency": "monthly", "day_of_month": 1, "hour": 4, "minute": 0, "retain": {"value": 12, "unit": "months"}}' \
  '{"name": "yearly",  "frequency": "yearly",  "month": 1, "day_of_month": 1, "hour": 5, "minute": 0, "retain": {"value": 7, "unit": "years"}}'
do
  nx -X POST $API/instances/$ID/snapshot-schedules -d "$BODY" | jq '{name, next_run_at}'
done

# What each one holds, and why a run took no snapshot
nx $API/instances/$ID/snapshot-schedules | jq '.items[] | {name, enabled, next_run_at, snapshots, last_error}'

# Pause one; delete one together with the snapshots it took
nx -X PATCH $API/snapshot-schedules/$SCHEDULE_ID -d '{"enabled": false}'
nx -X DELETE "$API/snapshot-schedules/$SCHEDULE_ID?delete_snapshots=true"

Rate limits and quotas

  • Requests are rate limited per account, shared by all of its users and tokens; the rate depends on your plan and is shown as max_api_rps in GET /v1/limits. Over the limit you get 429 rate_limited with Retry-After in seconds.
  • Quotas (instances, volumes, IPs and more, with current usage) are in GET /v1/limits. A create over a quota answers 409 with this body; ask for more with POST /v1/limit-requests:
HTTP/1.1 409 Conflict
{"error": "quota_exceeded", "key": "max_volumes", "limit": 10, "used": 10, "requested": 1,
 "message": "account quota max_volumes exceeded: limit 10, used 10, requested 1"}

Retries

GET requests are safe to retry. A POST that timed out may already have created the resource, and the API has no idempotency keys. Instance and volume names are unique among your live resources, so a retried create of one that got through answers 409 name_taken: then find it by name in the list and follow its job with GET /v1/jobs?resource_id={id}. Retry 429 and 503 after Retry-After.

Webhooks

Instead of polling, let Nextify tell you when something happens on your account. Add an HTTPS endpoint in the portal under Account → Webhooks (or with POST /v1/webhooks) and pick the event types it should get, or all of them. Nextify then sends each event as a signed JSON POST to that URL. An account can have up to 10 endpoints.

Events

TypeWhendata.object
instance.createdAn instance was created and is ready.instance
instance.started
instance.stopped
An instance was powered on or off.instance
instance.deletedAn instance was deleted.instance
instance.disk_resizedAn instance's root disk was grown.instance
instance.app_deployed
instance.app_deploy_failed
A deploy of an app from Git ended, at the first boot or after a redeploy (data.deploy has its id and status).instance
volume.created
volume.attached
volume.detached
volume.resized
volume.deleted
The volume change finished.volume
snapshot.created
snapshot.restored
snapshot.deleted
The snapshot change finished.snapshot
bucket.created
bucket.deleted
The bucket change finished.bucket
ip.attached
ip.detached
An extra public IP was attached to or detached from an instance.public IP
load_balancer.created
load_balancer.updated
load_balancer.deleted
A load balancer was created, changed or deleted.load balancer
job.succeededAny background job finished.job
job.failedA background job failed for good, after its retries.job
invoice.paidAn invoice of the account was paid.invoice
webhook.testYou sent a test event from the portal or the API.{message}

GET /v1/webhooks/event-types lists the types. An endpoint subscribed to ["*"] gets every type, including ones added later, except webhook.test, which only goes to the endpoint you send it to.

The request

POST https://hooks.example.com/nextify
Content-Type: application/json
User-Agent: Nextify-Webhooks/1.0 (+https://www.nextifynow.com/kb/webhooks)
Nextify-Event-Id: evt_4f1c9e0b7a2d4c8e9f0a1b2c3d4e5f60
Nextify-Event-Type: instance.created
Nextify-Delivery-Id: 81234
Nextify-Signature: t=1793564103,v1=5b9f0c...e41a

{"id": "evt_4f1c9e0b7a2d4c8e9f0a1b2c3d4e5f60", "type": "instance.created",
 "created": "2026-11-01T20:15:03Z", "api_version": "v1", "account_id": "6f0c2f4e-…", "livemode": true,
 "data": {"object": {"id": "…", "name": "web-1", "status": "running", "public_ip": "192.102.139.40", …},
          "job": {"id": "48213", "type": "instance.create", "status": "succeeded", "error": null}}}
  • data.object is the resource as it was at the moment of the event, with the same field names as its GET in the API (a subset). Job events and events caused by a job also carry data.job.
  • Nextify-Event-Id (the same as id in the body) stays the same on every retry. Store the ids you have handled and ignore a repeat.
  • The endpoint must be https:// on port 443 or 1024 to 65535, with a DNS name or a public IP address, and at most 2048 characters. Every address the name resolves to must be public, at every delivery.

Verifying the signature

Each endpoint has its own signing secret (whsec_...), shown once when you add the endpoint or rotate its secret. The Nextify-Signature header is t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of <t>.<raw body> with the whole secret, whsec_ included, as the key. To check a request:

  1. Read the body as raw bytes, before any JSON parsing: re-serialised JSON does not match.
  2. Compute the HMAC of t, a dot and the raw body, and compare it with v1 in constant time.
  3. Reject the request if t is more than 5 minutes old. A retry carries a fresh t and signature.

Node.js (Express):

const crypto = require("node:crypto");

function verifyNextify(rawBody, header, secret) {
  const f = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")));
  if (!/^\d+$/.test(f.t ?? "") || Date.now() / 1000 - Number(f.t) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(f.t + ".").update(rawBody).digest();
  const given = Buffer.from(f.v1 ?? "", "hex");
  return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}

app.post("/nextify", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyNextify(req.body, req.get("Nextify-Signature") ?? "", process.env.NEXTIFY_WEBHOOK_SECRET)) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(req.body);   // skip event.id if you have handled it already
  res.sendStatus(204);
});

Python:

import hashlib, hmac, time

def verify_nextify(raw_body: bytes, header: str, secret: str) -> bool:
    f = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
    t = f.get("t", "")
    if not t.isdigit() or time.time() - int(t) > 300:
        return False
    expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, f.get("v1", ""))

Answering, retries and the delivery log

  • Answer with any 2xx status within 10 seconds; do slow work after answering. Any other status, a timeout or a redirect (3xx is not followed) is a failure.
  • A failed delivery is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours and 10 hours, then every 12 hours, until 3 days after the event; then it is marked failed. A test event is tried once, and only an enabled endpoint can get one.
  • An endpoint where every delivery has failed for 5 days is disabled and its disabled_reason says so. Enabling it again (PATCH with "enabled": true) clears that.
  • The delivery log (GET /v1/webhooks/{id}/deliveries) keeps events for 30 days, with the request body and the first 2,000 characters of your answer. POST /v1/webhooks/deliveries/{delivery_id}/retry sends a delivered or failed one again at once.
  • Rotating the secret (POST /v1/webhooks/{id}/rotate-secret) stops the old one immediately.
  • A read-only API token can list endpoints and read the log; adding, changing, testing and resending need a full-access token.

Startup scripts, labels and secrets

POST /v1/instances takes three optional fields that set a server up without logging in to it:

  • user_data: a startup script, a shell script starting with #! or a cloud-config starting with #cloud-config, at most 64 KiB. It runs once, at the first boot, as root, after the network is up. Anything else answers 422 invalid_user_data. It is stored as you send it, so keep secrets out of it. Not allowed with from_snapshot_id.
  • env: up to 64 variables, [{"name": "API_KEY", "value": "…", "secret": true}]. Nextify writes them to /etc/nextify/env inside the server (readable by root only), one NAME='value' line each, before your startup script runs, together with NEXTIFY_INSTANCE_ID, NEXTIFY_INSTANCE_NAME and NEXTIFY_REGION. A script reads them with . /etc/nextify/env, a systemd service with EnvironmentFile=/etc/nextify/env. A secret's value is stored encrypted and never returned by the API: GET /v1/instances/{id}/env shows only a hint such as ••••abcd.
  • labels: up to 32 key=value pairs of your own, such as {"env": "staging"}. List by label with GET /v1/instances?label=env=staging&label=team (a bare key means "has this label"; every one given must match). Labels never change networking: firewalls target tags.

This creates an instance that installs nginx and answers only requests that carry an API key. The key is a secret that the script reads from /etc/nextify/env; it never appears in the script:

cat > setup.sh <<'EOF'
#!/bin/bash
set -euo pipefail
. /etc/nextify/env                      # API_KEY, APP_ENV and the NEXTIFY_* variables
export DEBIAN_FRONTEND=noninteractive
apt-get update && apt-get install -y nginx
cat > /etc/nginx/sites-available/default <<NGINX
server {
  listen 80 default_server;
  root /var/www/html;
  location / {
    if (\$http_x_api_key != "$API_KEY") { return 401; }
  }
}
NGINX
chmod 600 /etc/nginx/sites-available/default
echo "Hello from $NEXTIFY_INSTANCE_NAME ($APP_ENV)" > /var/www/html/index.html
systemctl reload nginx
EOF

NEW=$(jq -n --rawfile ud setup.sh --arg kid "$KEY_ID" --arg key "$MY_API_KEY" '{
    name: "web-2", machine_type: "sm1.s1-1", region: "us-west-1", image: "ubuntu-24.04",
    ssh_key_ids: [$kid], user_data: $ud, labels: {env: "staging", app: "web"},
    env: [{name: "API_KEY", value: $key, secret: true}, {name: "APP_ENV", value: "staging"}]
  }' | nx -X POST $API/instances -d @-)
ID=$(echo "$NEW" | jq -r .id)
wait_job "$(echo "$NEW" | jq -r .job_id)"

# Wait for the startup script to finish, then print its output.
until nx $API/instances/$ID/setup-log | jq -e '.status == "done" or .status == "error"' >/dev/null; do sleep 5; done
nx $API/instances/$ID/setup-log | jq -r '.status, .output'

IP=$(nx $API/instances/$ID | jq -r .public_ip)
curl -H "X-Api-Key: $MY_API_KEY" http://$IP/   # open port 80 in a security group first

nx "$API/instances?label=env=staging" | jq -r '.items[].name'
nx $API/instances/$ID/env | jq '.items[] | {name, value, hint}'   # the secret shows only its hint
  • GET /v1/instances/{id}/setup-log answers the setup status (running, done, error; unknown until first read) and the last 64 KiB of /var/log/cloud-init-output.log, as last read from the server. When that copy is more than 10 seconds old, the call also starts a new read and says "refreshing": true: ask again a few seconds later. A stopped instance answers its last copy, or 409 not_running if the log was never read. GET /v1/instances/{id}/user-data returns the script itself.
  • PUT /v1/instances/{id}/env replaces the whole set. Send {"name": "API_KEY", "secret": true} without a value to keep a secret as it is; a variable you leave out is removed. A running server gets the new file when the returned job_id succeeds, usually within a minute; a stopped one at its next start. Whatever already read the old file, such as a service or the nginx configuration above, keeps the old value until it reads the file again.
  • PUT /v1/instances/{id}/labels replaces the labels.

Resizing an instance

POST /v1/instances/{id}/resize moves an instance to another plan of the same line (Shared vCPU or Dedicated vCPU), up or down in vCPUs and memory. GET /v1/instances/{id}/resize-options lists the plans it can move to, each with direction (up or down), whether its disk can grow, and what the change does on the running server, with and without growing the disk (changes, changes_keep_disk). Prices are in GET /v1/machines?region=: the new plan's price applies from the change.

The disk: grow_disk (default true) grows the disk to the plan's size. Growing the disk is permanent: a disk cannot shrink, so afterwards you can only move to plans with at least that disk size. Keep the disk to keep smaller plans available; the price is the plan's either way.

  • On a running instance, added disk and added Shared vCPUs apply live. Fewer vCPUs, a Dedicated vCPU change and less memory need a restart of about a minute; added memory applies live where the region supports it. A stopped instance takes the whole change at once.
  • A change that needs a restart answers 409 restart_required with error.changes and changes nothing. Ask whoever owns the server, then send the same request with "restart": true. Nothing is restarted without it.
  • Secure and GPU VMs keep their size (409 not_resizable): create a new one of the size you need and move your data.
  • The answer carries a job_id (type instance.resize); the instance shows its new size once the job succeeds.
# The plans it can move to: direction, size, and whether a restart is needed
nx $API/instances/$ID/resize-options | jq -r '.options[]
  | "\(.machine_type)\t\(.direction)\t\(.vcpu) vCPU\t\(.ram_mib) MiB\t\(.disk_gb) GB\trestart: \(.restart_required)"'

# Move to sm1.s2-4 and keep the disk as it is, so smaller plans stay available
BODY='{"machine_type": "sm1.s2-4", "grow_disk": false}'
R=$(nx -X POST $API/instances/$ID/resize -d "$BODY")
if [ "$(echo "$R" | jq -r '.error.code // empty')" = restart_required ]; then
  echo "$R" | jq '.error.changes'                # which parts need the restart
  # Only once the restart (about a minute) is agreed:
  R=$(nx -X POST $API/instances/$ID/resize -d "$(echo "$BODY" | jq '. + {restart: true}')")
fi
wait_job "$(echo "$R" | jq -r .job_id)"
nx $API/instances/$ID | jq '{machine_type, vcpu, memory_bytes, disk_bytes}'

Deploying from Git

Send source instead of user_data to POST /v1/instances and Nextify deploys an app from a Git repository: at the first boot it clones the repository, builds it and runs it as the service nextify-app, which is restarted if it stops. A public repository needs only its https:// URL. For a private one, create a deploy key (POST /v1/deploy-keys), add its public_key to the repository as a read-only deploy key, and send the repository's SSH URL (git@github.com:org/repo.git) with deploy_key_id.

  • source.git takes url, and optionally ref (a branch or tag), runtime (auto by default, or node, python, go, ruby, php, static), build and start (one line each; empty uses the runtime's default, and a Procfile web: line comes first for start), port and deploy_key_id.
  • The app listens on $PORT (port, 8080 by default, never 22). The security group app-tcp-<port>, inbound TCP on that port from anywhere, is created when first needed and added to the instance.
  • env still applies: the build and the app get the variables and secrets in their environment.
  • The first deploy's output is the setup log. POST /v1/instances/{id}/redeploy pulls the latest commit, builds it and restarts the app; follow its job_id, then read the setup log, which the deploy's output replaces. It answers 409 no_app, not_running or in_progress when it cannot start.
  • A source the API cannot use answers 422 invalid_source with the reason: an https URL with a user name or token in it, an SSH URL without a deploy key, a deploy key with an https URL, port 22, or a command on more than one line.
# A deploy key for a private repository: add .public_key to the repository, read-only
KEY=$(nx -X POST $API/deploy-keys -d '{"name": "shop"}')
echo "$KEY" | jq -r .public_key        # GitHub: Settings → Deploy keys → Add deploy key
DK=$(echo "$KEY" | jq -r .id)

# An instance that clones, builds and runs the app (it listens on $PORT)
NEW=$(jq -n --arg dk "$DK" --arg kid "$KEY_ID" --arg pw "$DB_PASSWORD" '{
    name: "shop-1", machine_type: "sm1.s1-1", region: "us-west-1", image: "ubuntu-24.04", ssh_key_ids: [$kid],
    source: {git: {url: "git@github.com:your-org/shop.git", ref: "main", port: 3000, deploy_key_id: $dk}},
    env: [{name: "DATABASE_PASSWORD", value: $pw, secret: true}]
  }' | nx -X POST $API/instances -d @-)
ID=$(echo "$NEW" | jq -r .id)
wait_job "$(echo "$NEW" | jq -r .job_id)"
until nx $API/instances/$ID/setup-log | jq -e '.status == "done" or .status == "error"' >/dev/null; do sleep 5; done
nx $API/instances/$ID/setup-log | jq -r .output | tail -n 5

# After a push: pull, build and restart, then read the deploy's output
wait_job "$(nx -X POST $API/instances/$ID/redeploy | jq -r .job_id)"
nx $API/instances/$ID/setup-log | jq -r .output | tail -n 5

One-click apps

GET /v1/apps lists apps Nextify can install on a new instance at its first boot, such as WordPress, Gitea, Grafana, PostgreSQL and Redis; no token needed. Send app with its slug to POST /v1/instances. The answer's app.credentials holds the passwords Nextify generated for it: they are in this answer only, so store them right away. Inside the server they stay in /etc/nextify/env, readable by root.

  • app.inputs sets the app's own settings (inputs[] in the catalog: name, default, required, pattern).
  • The plan needs at least the app's min_ram_mib of memory (422 plan_too_small) and the image must be Ubuntu or Debian (422 image_not_supported). A bad input answers 422 invalid_input.
  • app cannot be combined with user_data, source or from_snapshot_id. GET /v1/apps/{slug} shows the script that will run.
  • next_step says what to open once it is installed; <ip> stands for the instance's public address. The setup log shows the install's progress.
# The catalog (public): slug, memory, ports and settings
curl -s $API/apps | jq -r '.items[] | "\(.slug)\t\(.min_ram_mib) MiB\t\(.ports_public)"'

# PostgreSQL with its own database name; store the generated password at once
NEW=$(jq -n --arg kid "$KEY_ID" '{
    name: "db-1", machine_type: "sm1.s1-1", region: "us-west-1", image: "ubuntu-24.04", ssh_key_ids: [$kid],
    app: {slug: "postgresql", inputs: {APP_DB: "shop", APP_DB_USER: "shop"}}
  }' | nx -X POST $API/instances -d @-)
echo "$NEW" | jq '.app.credentials'      # shown once: {"PG_PASSWORD": "..."}
echo "$NEW" | jq -r '.app.next_step'
wait_job "$(echo "$NEW" | jq -r .job_id)"
nx $API/instances/$(echo "$NEW" | jq -r .id)/setup-log | jq -r .output

Terraform

The Nextify Terraform provider manages SSH keys, instances, volumes, buckets, public IPs, private networks and more, and waits for each operation to finish. To get it, email support@nextifynow.com. Give it a full-access token:

export NEXTIFY_TOKEN=nxt_...
terraform apply

AI assistants (MCP)

Claude, ChatGPT, Cursor, VS Code and other MCP clients can use Nextify through its MCP server at https://mcp.nextifynow.com/mcp: sign in with your Nextify account, or send an API token as above. Set up your assistant → · What it can do

Endpoint reference

0 endpoints. Paths are relative to https://api.nextifynow.com.

Loading the reference…