Documentation

API v1

Automate nouva projects, services, deployments, and operations with the stable, versioned REST API.

Start with v1

Base URL

https://api.nouva.sh/v1

GET /openapi.json is public and describes only the customer-facing /v1 routes. It uses bearer authentication. Nouva does not publish a Swagger UI. Request and response bodies are JSON, except the bucket upload route, which takes multipart form data.

List projects

export NOUVA_API_TOKEN="nouva_v1_REPLACE_WITH_YOUR_TOKEN"
export NOUVA_API_BASE="https://api.nouva.sh/v1"

curl --include "$NOUVA_API_BASE/projects?limit=20" \
  --header "Authorization: Bearer $NOUVA_API_TOKEN"

Authentication and project reach

Current workspace admins create, rotate, and revoke API tokens in Dashboard Settings. A token secret is shown only once when it is created or rotated, so store it in your CI secret manager rather than source control.

Bearer token

Send Authorization: Bearer <token> on every authenticated request. Secrets start with nouva_v1_.

Project reach

Tokens can cover all workspace projects or an explicit project allowlist. Selected project tokens cannot create projects or use workspace-wide server routes.

Expiry

Every token expires between 1 and 365 days after creation, and defaults to 90 days. Non-expiring tokens are not supported.

Rotation and revocation

Rotating revokes the old secret immediately and issues a replacement that keeps the same scopes and project reach. A token also stops working if its owner loses workspace admin.

Scopes

Choose only the scopes your integration needs. Every write scope automatically includes its matching read scope.

AreaRead scopeWrite scope
Projectsprojects:readprojects:write
Environmentsenvironments:readenvironments:write
App and worker servicesservices:readservices:write
Databasesdatabases:readdatabases:write
Deploymentsdeployments:readdeployments:write
Logslogs:readRead only
Metricsmetrics:readRead only
Volumesvolumes:readvolumes:write
Backupsbackups:readbackups:write
Domainsdomains:readdomains:write
Bucketsbuckets:readbuckets:write
Serversservers:readRead only

Endpoint reference

Every path below is relative to https://api.nouva.sh/v1. The OpenAPI document is the authoritative source for request and response bodies.

Projects

Creating a project requires an all-projects token and a connected, online server.

MethodPathScopeDescription
GET/projectsprojects:readList projects.
POST/projectsprojects:writeCreate a project.
GET/projects/{projectId}projects:readRead one project.
PATCH/projects/{projectId}projects:writeRename a project.
DELETE/projects/{projectId}projects:writeDelete an empty project.
GET/projects/{projectId}/environmentsenvironments:readList environments.
POST/projects/{projectId}/environmentsenvironments:writeCreate an environment.

Services

These routes cover app and worker services only. Databases have their own routes.

MethodPathScopeDescription
GET/projects/{projectId}/servicesservices:readList app and worker services.
POST/projects/{projectId}/servicesservices:writeCreate a service and its first deployment.
GET/services/{serviceId}services:readRead one service.
PATCH/services/{serviceId}services:writeUpdate service configuration.

Databases

MethodPathScopeDescription
GET/projects/{projectId}/databasesdatabases:readList databases.
POST/projects/{projectId}/databasesdatabases:writeProvision a database.
GET/databases/{serviceId}databases:readRead one database.
PATCH/databases/{serviceId}databases:writeUpdate database configuration.
POST/databases/{serviceId}/actions/restartdatabases:writeRestart a database.
POST/databases/{serviceId}/actions/retry-provisiondatabases:writeRetry a failed provision.
POST/databases/{serviceId}/actions/postgres-pitrdatabases:writeStart a Postgres point-in-time restore.

Deployments and logs

MethodPathScopeDescription
GET/services/{serviceId}/deploymentsdeployments:readList deployments.
POST/services/{serviceId}/deploymentsdeployments:writeRedeploy the latest or a chosen deployment.
GET/deployments/{deploymentId}deployments:readRead one deployment.
POST/deployments/{deploymentId}/actions/restartdeployments:writeRestart the running deployment.
POST/deployments/{deploymentId}/actions/rollbackdeployments:writeRoll back to this deployment.
GET/deployments/{deploymentId}/logs/buildlogs:readRead build logs.
GET/deployments/{deploymentId}/logs/runtimelogs:readPage through runtime logs.
GET/schedule-runs/{runId}/logslogs:readPage through worker job logs.

Worker schedules

Worker schedules are governed by the services scopes, not a scope of their own.

MethodPathScopeDescription
GET/services/{serviceId}/schedulesservices:readList schedules for a worker.
POST/services/{serviceId}/schedulesservices:writeCreate a cron schedule.
PATCH/schedules/{scheduleId}services:writeUpdate or disable a schedule.
DELETE/schedules/{scheduleId}services:writeDelete a schedule.
GET/schedules/{scheduleId}/runsservices:readList schedule runs.
POST/schedules/{scheduleId}/runsservices:writeStart a run now.

Volumes and backups

MethodPathScopeDescription
GET/projects/{projectId}/volumesvolumes:readList volumes.
GET/volumes/{volumeId}volumes:readRead one volume.
PATCH/volumes/{volumeId}volumes:writeRename a volume.
DELETE/volumes/{volumeId}volumes:writeQueue volume deletion.
POST/volumes/{volumeId}/actions/attachvolumes:writeStage a volume attachment.
POST/volumes/{volumeId}/actions/increase-reservationvolumes:writeIncrease the reserved size.
GET/volumes/{volumeId}/backupsbackups:readList backups for a volume.
GET/backups/{backupId}backups:readRead one backup.
PATCH/backups/{backupId}backups:writeLock or unlock a backup.
DELETE/backups/{backupId}backups:writeQueue backup deletion.
POST/services/{serviceId}/backupsbackups:writeTake a backup now.
GET/services/{serviceId}/backups/schedulebackups:readRead the backup schedule.
PATCH/services/{serviceId}/backups/schedulebackups:writeToggle daily, weekly, and monthly backups.
POST/backups/{backupId}/actions/restorebackups:writeRestore a backup into a pending volume.
POST/services/{serviceId}/actions/apply-restorebackups:writeCut the service over to the restored volume.

Domains

MethodPathScopeDescription
GET/services/{serviceId}/domainsdomains:readList custom domains.
POST/services/{serviceId}/domainsdomains:writeAdd a custom domain.
DELETE/services/{serviceId}/domains/{domainId}domains:writeRemove a custom domain.

Buckets

MethodPathScopeDescription
GET/projects/{projectId}/bucketsbuckets:readList buckets.
POST/projects/{projectId}/bucketsbuckets:writeCreate a bucket.
GET/buckets/{bucketId}buckets:readRead a bucket and its per-environment instances.
DELETE/buckets/{bucketId}buckets:writeQueue bucket deletion.
GET/buckets/{bucketId}/objectsbuckets:readList objects and prefixes.
GET/buckets/{bucketId}/objects/download-urlbuckets:readGet a public or signed download URL.
POST/buckets/{bucketId}/objects/uploadbuckets:writeUpload one object as multipart form data.
DELETE/buckets/{bucketId}/objectsbuckets:writeDelete objects by key or prefix.

Servers and metrics

Every server route requires an all-projects token. Selected-project tokens receive 403.

MethodPathScopeDescription
GET/serversservers:readList servers.
GET/servers/{serverId}servers:readRead one server.
GET/servers/{serverId}/healthservers:readRead derived status and readiness.
GET/servers/{serverId}/capabilitiesservers:readRead agent capability flags.
GET/services/{serviceId}/metricsmetrics:readRead service CPU, memory, network, and disk series.
GET/volumes/{volumeId}/metricsmetrics:readRead volume usage series.
GET/servers/{serverId}/metricsmetrics:read + servers:readRead host-level metrics.

Responses, headers, and errors

Nouva never runs your workloads directly, so most mutations return 202 and queue work for the agent on your server. Poll the resource until it reaches a terminal state instead of assuming the response body is final.

StatusWhen you see it
200A read, or a mutation that completed inside the control plane.
201A record was created and needs no agent work.
202Work was queued for your server. Poll the resource for the outcome.
204The delete completed and there is no body.

Every response includes a server-generated X-Request-Id. Keep that ID when contacting support or correlating API activity. Asynchronous responses usually also carry a Location header pointing at the resource to poll. Errors use one stable envelope.

Error response

{
  "error": {
    "code": "stable_snake_case_code",
    "message": "Safe actionable message",
    "requestId": "00000000-0000-4000-8000-000000000000",
    "details": []
  }
}
Error codeStatusMeaning
authentication_required401No bearer token was supplied.
invalid_token401The token is malformed or not recognized.
token_expired401The token passed its expiry date.
token_revoked401The token was revoked or rotated away.
token_owner_disabled401The owning user is no longer a workspace admin.
insufficient_scope403The token lacks the required scope.
project_not_allowed403The project is outside the token's project reach.
not_found404The resource does not exist, or belongs to another workspace.
validation_failed400The request did not match the contract.
idempotency_key_required400A POST route was called without an Idempotency-Key.
conflict409The resource conflicts with existing state.
idempotency_conflict409The key was reused with a different payload, or is still in flight.
precondition_failed412 or 413The resource is not in a state that allows the operation.
rate_limited429A rate-limit window was exhausted.
dependency_unavailable503A required Nouva dependency was unavailable.
internal_error500An unexpected error. Report the request ID.

Resources outside your token workspace return 404 rather than 403, so probing IDs cannot confirm that they exist. Error messages are deliberately generic and never echo credentials or provider diagnostics.

Pagination and idempotency

Collection endpoints use versioned keyset cursors based on creation time and ID. The default page size is 20 and the maximum is 100. Pass the previous nextCursor back as ?cursor= to walk a collection; a cursor from a different collection is rejected with 400.

Paginated response

{
  "items": [],
  "nextCursor": "opaque-versioned-cursor",
  "hasMore": false
}

Every POST route requires an Idempotency-Key header of at most 255 characters. Reuse the same key only for the same request. Matching completed requests replay safely; a different payload or a concurrent in-flight reuse returns 409. Keys are retained for 24 hours. PATCH and DELETE routes do not take the header.

Multipart bucket uploads also require a lowercase SHA-256 contentSha256 query parameter. Compute it from the file bytes before upload. It is part of the idempotency fingerprint, so a changed artifact cannot replay an earlier upload. Uploads carry one file per request and are capped at 1 GiB by default.

Upload one bucket object

FILE="./artifact.tar.gz"
CONTENT_SHA256="$(shasum -a 256 "$FILE" | awk '{print $1}')"

curl --fail-with-body --request POST \
  "$NOUVA_API_BASE/buckets/$NOUVA_BUCKET_ID/objects/upload?environmentId=$NOUVA_ENVIRONMENT_ID&key=artifacts/artifact.tar.gz&contentSha256=$CONTENT_SHA256" \
  --header "Authorization: Bearer $NOUVA_API_TOKEN" \
  --header "Idempotency-Key: $CI_KEY-upload" \
  --form "file=@$FILE"

Run a deployment from CI

set -euo pipefail

export NOUVA_API_TOKEN="${NOUVA_API_TOKEN:?Set a CI secret}"
export NOUVA_API_BASE="https://api.nouva.sh/v1"
export NOUVA_PROJECT_NAME="${NOUVA_PROJECT_NAME:-ci-project}"
export NOUVA_SERVICE_NAME="${NOUVA_SERVICE_NAME:-ci-app}"

request() {
  curl --fail-with-body --silent --show-error \
    --header "Authorization: Bearer $NOUVA_API_TOKEN" "$@"
}

CI_KEY="ci-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
PROJECT_ID="${NOUVA_PROJECT_ID:-}"

# Select the requested project, or create it once with a CI-derived key.
if [ -z "$PROJECT_ID" ]; then
  PROJECT_ID="$(request "$NOUVA_API_BASE/projects?limit=100" | \
    jq -r --arg name "$NOUVA_PROJECT_NAME" '.items[] | select(.name == $name) | .id' | head -n 1)"
fi
if [ -z "$PROJECT_ID" ]; then
  PROJECT_BODY="$(jq -nc --arg name "$NOUVA_PROJECT_NAME" '{name: $name}')"
  PROJECT_ID="$(request --request POST "$NOUVA_API_BASE/projects" \
    --header "Idempotency-Key: $CI_KEY-project" \
    --header "Content-Type: application/json" \
    --data "$PROJECT_BODY" | jq -er '.project.id')"
fi

# Select the service, or create an app from the connected GitHub installation.
SERVICE_ID="$(request "$NOUVA_API_BASE/projects/$PROJECT_ID/services?limit=100" | \
  jq -r --arg name "$NOUVA_SERVICE_NAME" '.items[] | select(.name == $name) | .id' | head -n 1)"
if [ -z "$SERVICE_ID" ]; then
  : "${NOUVA_GITHUB_INSTALLATION_ID:?Set the connected GitHub installation ID}"
  : "${NOUVA_GITHUB_REPOSITORY_ID:?Set the GitHub repository ID}"
  SERVICE_BODY="$(jq -nc \
    --arg name "$NOUVA_SERVICE_NAME" \
    --argjson installationId "$NOUVA_GITHUB_INSTALLATION_ID" \
    --argjson repoId "$NOUVA_GITHUB_REPOSITORY_ID" \
    '{type: "app", name: $name, installationId: $installationId, repoId: $repoId,
      branch: "main", appBuildType: "railpack", appBuildConfig: {buildRoot: "."},
      resourceLimits: null, storageSizeGb: 0}')"
  CREATE_RESPONSE="$(request --request POST "$NOUVA_API_BASE/projects/$PROJECT_ID/services" \
    --header "Idempotency-Key: $CI_KEY-service" \
    --header "Content-Type: application/json" \
    --data "$SERVICE_BODY")"
  SERVICE_ID="$(printf %s "$CREATE_RESPONSE" | jq -er '.service.id')"
  DEPLOYMENT_ID="$(printf %s "$CREATE_RESPONSE" | jq -er '.deployment.id')"
else
  DEPLOYMENT_ID="$(request --request POST "$NOUVA_API_BASE/services/$SERVICE_ID/deployments" \
    --header "Idempotency-Key: $CI_KEY-deployment" \
    --header "Content-Type: application/json" \
    --data '{}' | jq -er '.deployment.id')"
fi

# Poll a terminal deployment state, then fetch a bounded runtime-log page.
for attempt in $(seq 1 60); do
  DEPLOYMENT="$(request "$NOUVA_API_BASE/deployments/$DEPLOYMENT_ID")"
  STATUS="$(printf %s "$DEPLOYMENT" | jq -er '.deployment.status')"
  case "$STATUS" in
    LIVE) break ;;
    FAILED|REPLACED|REMOVED) printf '%s\n' "$DEPLOYMENT" >&2; exit 1 ;;
  esac
  sleep 5
done

if [ "$STATUS" != "LIVE" ]; then
  printf 'Deployment did not reach LIVE before timeout (last status: %s)\n' "$STATUS" >&2
  exit 1
fi

request "$NOUVA_API_BASE/deployments/$DEPLOYMENT_ID/logs/runtime?limit=200&mode=initial&type=all" | jq .

Logs and metrics

Logs

  • Runtime and worker-job log routes accept limit (1 to 500, default 200), mode (initial or older), type (stdout, stderr, or all), and cursor.
  • Page backwards by passing the returned nextCursor with mode=older.
  • Build logs return the last 500 entries and are not paginated.
  • Runtime logs are retained for seven days.
  • Values that match your service or deployment environment variables are redacted.

Metrics

  • Metric routes accept range of 1h, 6h, 1d, 7d, or 30d, defaulting to 1h.
  • Worker services also accept replicaIndex to scope the series to one continuous replica.
  • Responses carry a status of ready, pending, stale, or unavailable alongside a human-readable message, so treat a missing series as a normal outcome.
  • Metrics are retained for 30 days.

Rate limits and compatibility

Rate limits

  • 120 requests per minute per token.
  • 600 requests per minute per workspace.
  • 30 invalid authentication attempts per minute per trusted client IP.
  • Rate-limit headers are returned with responses.

Compatibility

  • Changes within /v1 are backward-compatible and additive.
  • Breaking changes require a new version such as /v2.
  • Nouva documents deprecations at least 90 days before removal.
Header prefixWindow
X-RateLimit-Per token.
X-Workspace-RateLimit-Per workspace.
X-Invalid-Auth-RateLimit-Per client IP, returned on invalid-authentication responses.

Each prefix carries Limit, Remaining, and Reset, where Reset is a Unix timestamp in seconds. If the API rate-limit store is unavailable, requests fail closed with 503.