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/v1OpenAPI document
https://api.nouva.sh/openapi.jsonGET /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.
| Area | Read scope | Write scope |
|---|---|---|
| Projects | projects:read | projects:write |
| Environments | environments:read | environments:write |
| App and worker services | services:read | services:write |
| Databases | databases:read | databases:write |
| Deployments | deployments:read | deployments:write |
| Logs | logs:read | Read only |
| Metrics | metrics:read | Read only |
| Volumes | volumes:read | volumes:write |
| Backups | backups:read | backups:write |
| Domains | domains:read | domains:write |
| Buckets | buckets:read | buckets:write |
| Servers | servers:read | Read 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.
| Method | Path | Scope | Description |
|---|---|---|---|
GET | /projects | projects:read | List projects. |
POST | /projects | projects:write | Create a project. |
GET | /projects/{projectId} | projects:read | Read one project. |
PATCH | /projects/{projectId} | projects:write | Rename a project. |
DELETE | /projects/{projectId} | projects:write | Delete an empty project. |
GET | /projects/{projectId}/environments | environments:read | List environments. |
POST | /projects/{projectId}/environments | environments:write | Create an environment. |
Services
These routes cover app and worker services only. Databases have their own routes.
| Method | Path | Scope | Description |
|---|---|---|---|
GET | /projects/{projectId}/services | services:read | List app and worker services. |
POST | /projects/{projectId}/services | services:write | Create a service and its first deployment. |
GET | /services/{serviceId} | services:read | Read one service. |
PATCH | /services/{serviceId} | services:write | Update service configuration. |
Databases
| Method | Path | Scope | Description |
|---|---|---|---|
GET | /projects/{projectId}/databases | databases:read | List databases. |
POST | /projects/{projectId}/databases | databases:write | Provision a database. |
GET | /databases/{serviceId} | databases:read | Read one database. |
PATCH | /databases/{serviceId} | databases:write | Update database configuration. |
POST | /databases/{serviceId}/actions/restart | databases:write | Restart a database. |
POST | /databases/{serviceId}/actions/retry-provision | databases:write | Retry a failed provision. |
POST | /databases/{serviceId}/actions/postgres-pitr | databases:write | Start a Postgres point-in-time restore. |
Deployments and logs
| Method | Path | Scope | Description |
|---|---|---|---|
GET | /services/{serviceId}/deployments | deployments:read | List deployments. |
POST | /services/{serviceId}/deployments | deployments:write | Redeploy the latest or a chosen deployment. |
GET | /deployments/{deploymentId} | deployments:read | Read one deployment. |
POST | /deployments/{deploymentId}/actions/restart | deployments:write | Restart the running deployment. |
POST | /deployments/{deploymentId}/actions/rollback | deployments:write | Roll back to this deployment. |
GET | /deployments/{deploymentId}/logs/build | logs:read | Read build logs. |
GET | /deployments/{deploymentId}/logs/runtime | logs:read | Page through runtime logs. |
GET | /schedule-runs/{runId}/logs | logs:read | Page through worker job logs. |
Worker schedules
Worker schedules are governed by the services scopes, not a scope of their own.
| Method | Path | Scope | Description |
|---|---|---|---|
GET | /services/{serviceId}/schedules | services:read | List schedules for a worker. |
POST | /services/{serviceId}/schedules | services:write | Create a cron schedule. |
PATCH | /schedules/{scheduleId} | services:write | Update or disable a schedule. |
DELETE | /schedules/{scheduleId} | services:write | Delete a schedule. |
GET | /schedules/{scheduleId}/runs | services:read | List schedule runs. |
POST | /schedules/{scheduleId}/runs | services:write | Start a run now. |
Volumes and backups
| Method | Path | Scope | Description |
|---|---|---|---|
GET | /projects/{projectId}/volumes | volumes:read | List volumes. |
GET | /volumes/{volumeId} | volumes:read | Read one volume. |
PATCH | /volumes/{volumeId} | volumes:write | Rename a volume. |
DELETE | /volumes/{volumeId} | volumes:write | Queue volume deletion. |
POST | /volumes/{volumeId}/actions/attach | volumes:write | Stage a volume attachment. |
POST | /volumes/{volumeId}/actions/increase-reservation | volumes:write | Increase the reserved size. |
GET | /volumes/{volumeId}/backups | backups:read | List backups for a volume. |
GET | /backups/{backupId} | backups:read | Read one backup. |
PATCH | /backups/{backupId} | backups:write | Lock or unlock a backup. |
DELETE | /backups/{backupId} | backups:write | Queue backup deletion. |
POST | /services/{serviceId}/backups | backups:write | Take a backup now. |
GET | /services/{serviceId}/backups/schedule | backups:read | Read the backup schedule. |
PATCH | /services/{serviceId}/backups/schedule | backups:write | Toggle daily, weekly, and monthly backups. |
POST | /backups/{backupId}/actions/restore | backups:write | Restore a backup into a pending volume. |
POST | /services/{serviceId}/actions/apply-restore | backups:write | Cut the service over to the restored volume. |
Domains
| Method | Path | Scope | Description |
|---|---|---|---|
GET | /services/{serviceId}/domains | domains:read | List custom domains. |
POST | /services/{serviceId}/domains | domains:write | Add a custom domain. |
DELETE | /services/{serviceId}/domains/{domainId} | domains:write | Remove a custom domain. |
Buckets
| Method | Path | Scope | Description |
|---|---|---|---|
GET | /projects/{projectId}/buckets | buckets:read | List buckets. |
POST | /projects/{projectId}/buckets | buckets:write | Create a bucket. |
GET | /buckets/{bucketId} | buckets:read | Read a bucket and its per-environment instances. |
DELETE | /buckets/{bucketId} | buckets:write | Queue bucket deletion. |
GET | /buckets/{bucketId}/objects | buckets:read | List objects and prefixes. |
GET | /buckets/{bucketId}/objects/download-url | buckets:read | Get a public or signed download URL. |
POST | /buckets/{bucketId}/objects/upload | buckets:write | Upload one object as multipart form data. |
DELETE | /buckets/{bucketId}/objects | buckets:write | Delete objects by key or prefix. |
Servers and metrics
Every server route requires an all-projects token. Selected-project tokens receive 403.
| Method | Path | Scope | Description |
|---|---|---|---|
GET | /servers | servers:read | List servers. |
GET | /servers/{serverId} | servers:read | Read one server. |
GET | /servers/{serverId}/health | servers:read | Read derived status and readiness. |
GET | /servers/{serverId}/capabilities | servers:read | Read agent capability flags. |
GET | /services/{serviceId}/metrics | metrics:read | Read service CPU, memory, network, and disk series. |
GET | /volumes/{volumeId}/metrics | metrics:read | Read volume usage series. |
GET | /servers/{serverId}/metrics | metrics:read + servers:read | Read 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.
| Status | When you see it |
|---|---|
200 | A read, or a mutation that completed inside the control plane. |
201 | A record was created and needs no agent work. |
202 | Work was queued for your server. Poll the resource for the outcome. |
204 | The 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 code | Status | Meaning |
|---|---|---|
authentication_required | 401 | No bearer token was supplied. |
invalid_token | 401 | The token is malformed or not recognized. |
token_expired | 401 | The token passed its expiry date. |
token_revoked | 401 | The token was revoked or rotated away. |
token_owner_disabled | 401 | The owning user is no longer a workspace admin. |
insufficient_scope | 403 | The token lacks the required scope. |
project_not_allowed | 403 | The project is outside the token's project reach. |
not_found | 404 | The resource does not exist, or belongs to another workspace. |
validation_failed | 400 | The request did not match the contract. |
idempotency_key_required | 400 | A POST route was called without an Idempotency-Key. |
conflict | 409 | The resource conflicts with existing state. |
idempotency_conflict | 409 | The key was reused with a different payload, or is still in flight. |
precondition_failed | 412 or 413 | The resource is not in a state that allows the operation. |
rate_limited | 429 | A rate-limit window was exhausted. |
dependency_unavailable | 503 | A required Nouva dependency was unavailable. |
internal_error | 500 | An 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(initialorolder),type(stdout,stderr, orall), andcursor. - Page backwards by passing the returned
nextCursorwithmode=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
rangeof1h,6h,1d,7d, or30d, defaulting to1h. - Worker services also accept
replicaIndexto scope the series to one continuous replica. - Responses carry a
statusofready,pending,stale, orunavailablealongside a human-readablemessage, 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
/v1are backward-compatible and additive. - Breaking changes require a new version such as
/v2. - Nouva documents deprecations at least 90 days before removal.
| Header prefix | Window |
|---|---|
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.