REST API

The address, how a script signs in, how the organization is chosen, what errors look like, and worked examples with curl.

Everything the console and the CLI do, they do through one HTTP API. The CLI is built on it and has no private door: whatever isogrid can do, a script with the same credential can do with curl.

This page is a guide to using that API: the address, how a script signs in, and the calls most teams need. It does not list every endpoint. If you need one that is not shown here, the CLI covers the same ground, and the team will give you the exact call on request.

The address

Every call goes to https://api.isogrid.skyvault.pro/api/v1.

The examples below describe what the API accepts today. Check them again after a platform upgrade rather than assuming a field is still there.

Signing in from a script

Every call carries a bearer token:

Authorization: Bearer <token>

For a script, that token is a CLI credential: a long-lived token that starts with skv_, minted by you, in your name, for one organization. You create it with the CLI and approve it in the browser. There is no form in the console that hands out a token without that approval.

export ISOGRID_CONFIG_HOME="$(mktemp -d)"
isogrid --api https://api.isogrid.skyvault.pro login --name my-script
jq -r '.contexts[].token' "$ISOGRID_CONFIG_HOME/config.json"
rm -rf "$ISOGRID_CONFIG_HOME"

The temporary ISOGRID_CONFIG_HOME keeps the sign-in on your own machine untouched. Installing the CLI covers getting isogrid.

What to know about the credential:

  • It is pinned to the organization shown on the approval page. Check it before you approve.
  • It never exceeds what you hold. Its permissions are intersected with your own role and groups on every request, so reducing your access reduces the script's too.
  • Narrow it. Add --scope with only what the script needs, for example --scope application:create,application:write-any. Without --scope, the approval page proposes what suits your role and you untick the rest. See Deploying from CI/CD for the scopes a deployment needs.
  • It lasts 90 days unless you chose a shorter life on the approval page.
  • Some things it can never do, whatever it asks for: billing, appointing administrators, and minting or revoking credentials. Those need you, signed in to the console.
  • Revoke it from the console's CLI page when the script is retired or the token may have leaked.

The examples below assume:

export ISOGRID_API=https://api.isogrid.skyvault.pro/api/v1
export ISOGRID_TOKEN=skv_...

Keep the token out of your shell history and out of your repository, the same way you would a password.

Choosing the organization

A person can belong to several organizations, so anything that belongs to one needs to know which. You say so with a header, giving the organization's slug or its id:

X-Organization: acme

The same value is accepted as a query parameter, ?organization=acme, for the cases where a header is awkward.

With a CLI credential you can leave it out: the credential already belongs to one organization and every call acts there. Naming a different one is refused with 403, and the error says which organization the credential is bound to.

What an error looks like

Every refusal has the same shape, whatever went wrong:

{
  "error": {
    "code": "permission_denied",
    "message": "Your role in Acme (developer) does not allow this",
    "details": {
      "required_permission": "application:create",
      "role": "developer",
      "organization": "acme"
    }
  }
}

message is written to be shown to a person. code is what a script should branch on. details varies with the error and may be empty.

Status code Meaning
401 unauthenticated No token, or one that is unknown, expired or revoked.
402 insufficient_credits The organization cannot pay for what was asked. details says how much was needed.
403 permission_denied You, or this credential, may not do this.
404 not_found It does not exist, or it is not yours to see.
409 conflict It clashes with what exists: a name already taken, or a product with no price in that region yet.
422 validation_error The request is malformed. details.fields names each field and what is wrong with it.
503 no_capacity The platform cannot host this right now. The request was not wrong; retry later.

With curl, use -f (or --fail-with-body, to keep the message) so a refusal fails your script instead of being piped into the next command.

Lists and pages

The main lists (applications, databases, database instances, networks, regions, images) return one page at a time:

{ "items": [ ... ], "total": 37, "limit": 50, "offset": 0 }

limit is 50 by default and 200 at most. offset is where the page starts. To read everything, add limit to offset until it reaches total.

Not every list is paged this way. A few short ones (an application's builds, the instance types of a region) return a plain array. The examples below show which shape each one has.

Things that take time

Creating something returns before it is running. The answer tells you the thing exists and gives you its id; you then read its status until it settles.

You asked for The call answers Then read Until
An application 201, with the application GET /applications/{id}/status deployment_status is running, or failed
A build 202, with a build_id GET /applications/{id}/builds/{build_id} status is succeeded, or one of failed, no_dockerfile, restricted, cancelled
A redeployment 202 GET /applications/{id}/status as for an application
A database 202, with the database GET /databases/{id} status is ready, or failed

An application's deployment_status is one of none, queued, deploying, starting, running, degraded, stopped or failed. degraded means it is up but not wholly: fewer copies running than asked for, or copies that keep restarting. The message beside it says why in words.

Poll every few seconds, not in a tight loop. Build output is read the same way: GET /applications/{id}/builds/{build_id}/logs?after=0 returns the lines so far and a last_id; pass that as after to get only what is new.

Worked examples

Who am I, and where can I deploy

curl -fsS "$ISOGRID_API/me" -H "Authorization: Bearer $ISOGRID_TOKEN"

returns your account and the organizations you belong to. The regions open to you, and the instance types a region offers with their prices:

curl -fsS "$ISOGRID_API/clusters" -H "Authorization: Bearer $ISOGRID_TOKEN" \
  | jq '.items[] | {id, slug, name}'

curl -fsS "$ISOGRID_API/pricing/applications/$CLUSTER_ID" \
  -H "Authorization: Bearer $ISOGRID_TOKEN" \
  | jq '.[] | {id, name, vcpu, memory_mb, prices}'

In the API a region is a cluster, and an instance type is a resource_tier.

List your applications

curl -fsS "$ISOGRID_API/applications?limit=20" \
  -H "Authorization: Bearer $ISOGRID_TOKEN" \
  | jq '.items[] | {id, slug, deployment_status, public_url}'

Create an application from an image

curl -fsS -X POST "$ISOGRID_API/applications" \
  -H "Authorization: Bearer $ISOGRID_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "hello",
    "source": "registry",
    "external_image": "nginx:1.27",
    "port": 80,
    "is_public": true
  }'

The answer is the application, with its id, its slug, and the address it will answer on in public_url. Its first deployment is already queued.

What the fields mean, and what happens when you leave one out:

Field Meaning Left out
name Required.
source registry for an image; github or gitlab to build from a repository. github
external_image An image in any registry, in full: nginx:1.27, ghcr.io/acme/web:main.
external_registry_id The stored registry whose credentials pull it. The image must be public.
cluster_id The region. The region of the network you named, else the platform's default region.
resource_tier_id The instance type. The smallest the region offers.
network_id The private network it runs on. Your organization's own network in that region.
port The port the container listens on. 8080 for an image.
replicas Number of copies, 1 to 10. 1
environment Non-secret settings, as an object of strings. None.
is_public Whether it is reachable from the internet. false

An image in your organization's own registry is named with image_id instead (GET /images lists them). To build from a repository, send repository_full_name (acme/api) and, if it is not main, default_branch; the repository must already be connected to your organization, as described in Git repositories.

Secrets do not go in environment. Send them in secrets; they are stored apart and never returned by any call.

Read its status

curl -fsS "$ISOGRID_API/applications/$APP_ID/status" \
  -H "Authorization: Bearer $ISOGRID_TOKEN"
{
  "application_id": "5b0c...",
  "deployment_status": "running",
  "desired": 1,
  "running": 1,
  "message": null,
  "checked_at": "2026-10-07T09:12:44Z",
  "listens": true,
  "tasks": [ ... ]
}

desired is the number of copies asked for and running the number up now. To wait in a script:

while :; do
  STATUS=$(curl -fsS "$ISOGRID_API/applications/$APP_ID/status" \
    -H "Authorization: Bearer $ISOGRID_TOKEN" | jq -r '.deployment_status')
  echo "$STATUS"
  case "$STATUS" in running|degraded|stopped|failed) break ;; esac
  sleep 5
done

Deploy again

For an application that runs an image, redeploy it:

curl -fsS -X POST "$ISOGRID_API/applications/$APP_ID/deploy" \
  -H "Authorization: Bearer $ISOGRID_TOKEN"

For one built from a repository, start a build of a branch, a tag or a commit; it is deployed when the build succeeds:

curl -fsS -X POST "$ISOGRID_API/applications/$APP_ID/builds" \
  -H "Authorization: Bearer $ISOGRID_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"git_ref": "main"}'

List your databases

curl -fsS "$ISOGRID_API/databases" -H "Authorization: Bearer $ISOGRID_TOKEN" \
  | jq '.items[] | {id, name, engine, status}'

These are the databases created on a shared server. Database servers of your own are a separate list, GET /database-instances. A database's status is one of pending, creating, importing, ready, failed or deleting.

Connection details, password included, are a separate call (GET /databases/{id}/connection) with a separate permission: being able to see that a database exists is not the same as being handed its credentials.

Edge cases worth knowing before you meet them

A 401 on a token that worked yesterday. It expired or was revoked. Mint a new one; nothing else changes. The answer is deliberately the same for an unknown, an expired and a revoked token.

A 403 that names another organization. You sent X-Organization for an organization the credential is not bound to. Drop the header, or mint a credential while the console is showing the organization you mean.

A 409 on create that mentions a price. That product has no price in that region yet, so it is not for sale there. Choose another region or ask. See Prices and billing.

A 402 on is_public. Your organization owes for earlier usage, and publishing is withheld until it is settled. Create it private, or top up.

The create call succeeded and the application failed. The call only promised that the application exists. Read /status, then the build or deployment log. When things go wrong covers the usual causes.

Money is in nanos. Amounts ending in _nanos are billionths of the currency unit: 1500000000 is 1.5. The prices object beside them carries the same figure already formatted for display.

What to read next