Platform automation and diagnostics

This page is the machine contract for scripts, CI pipelines and AI agents that create and debug DanubeData resources without a human watching.

Every managed resource — VPS, databases, cache, queues, object storage, static sites, managed apps, uptime checks, metric alerts, block volumes — reports state through the same status_details model and answers the same diagnostic questions. Learn it once and it applies to every product.

Field names and value domains here change additively, never in place.

Rapids containers use this same model, plus a few surfaces unique to them (preflight, probe, revisions). See Rapids automation.

Authentication and project scope

All requests use a bearer token against https://danubedata.ro:

Bash
curl -H "Authorization: Bearer $DANUBE_TOKEN" \
     -H "X-Team-Id: 42" \
     https://danubedata.ro/api/v1/cache

X-Team-Id selects the project. Omit it and the account default is used, which is rarely what automation wants. With the CLI:

Bash
danube --project 42 cache get my-redis --json
danube project select --project 42          # persists the default, no prompt

Token abilities

Abilities follow one pattern across products: {product}:read, {product}:write, {product}:delete, and — where a product exposes workload output or cluster internals — {product}:diagnostics.

ProductAbility prefix
VPS instances and block volumesvps:
Databasesdatabase:
Cachecache:
Queuesqueue:
Object storagestorage:
Static sitesstatic-site:
Managed appsapp:
Uptime checksuptime:
Metric alertsmetric-alert:
Container registryregistry:
Rapids containersserverless:

:diagnostics is deliberately separate from :read. A token that may list your databases should not automatically be able to read what those databases print to their logs. Grant it only to automation that needs to debug.

The status model

Every resource response carries status_details. Read this rather than the legacy status string — it is the same object the dashboard uses, so they cannot disagree with you.

JSON
{
  "summary": "failed",
  "health": "unhealthy",
  "observed_at": "2026-08-05T18:59:00+00:00",
  "stale": false,
  "operation": { "state": "failed", "terminal": true },
  "error": {
    "code": "cache.oom_killed",
    "source": "kubernetes",
    "resource": { "kind": "Pod", "name": "redis-my-cache-0" },
    "reason": "OOMKilling",
    "message": "The container exceeded its memory limit and was killed.",
    "retryable": false,
    "observed_at": "2026-08-05T18:59:00+00:00"
  }
}
FieldMeaning
summarypending, in_progress, ready, degraded, failed, stopped, deleting, unknown
healthhealthy, degraded, unhealthy, unknown — what is currently serving
operation.statequeued, running, succeeded, failed, cancelled
operation.terminalThe stop condition for polling
observed_atWhen the platform last checked — not when the row was last written
staleThe platform could not confirm the state; the summary is the last known one
errorPresent only while summary is failed or degraded; null otherwise

Three dimensions, because one string cannot answer three different questions: what is happening (summary), is my service working right now (health), and may I stop waiting (operation.terminal).

Polling correctly

Poll until operation.terminal is true. Never infer terminality from the status string, and never treat health: "unknown" as a failure — during a create or a rollout the platform genuinely does not yet know, and saying so is more honest than guessing.

Bash
until [ "$(danube database get my-pg --json | jq -r '.status_details.operation.terminal')" = "true" ]; do
  sleep 5
done

A status string you do not recognise must be treated as still running, not as finished. New states are added additively; terminal is what protects you.

stale: true

The state shown is the last one confirmed, and an in-flight operation has gone unconfirmed for 15 minutes. It is a statement about the platform's confidence, not about your resource.

A settled state never goes stale — stale: false alongside an old observed_at is correct and expected. An error established an hour ago is still an error.

degraded is terminal but not an outage

For resources with replicas, degraded usually means a replica is unhealthy while the primary keeps serving. Automation that treats degraded as "down" will page you for a working system.

Capabilities: what to call before calling it

Every resource reports which diagnostic surfaces it can serve, so an agent can branch without probing and eating a 404 or 403:

JSON
"capabilities": { "logs": true, "events": true, "diagnose": true }

These are derived, not hardcoded. A resource whose events are genuinely unavailable reports events: false rather than advertising an endpoint that will fail. Static sites additionally report build_logs.

Productstatus_details/logs/events/diagnose/metrics
Cache
Databases
Queues
Managed apps
VPS
VPS block volumes
Object storage
Static sitesbuild logs
Uptime checks
Metric alerts
Rapids containers
Container registry

VPS has no /logs by design. Guest operating system output is yours, not ours; we do not read inside your VM. Platform-side events are exposed instead.

Metric alerts have no /diagnose. An alert's history is its diagnosis — /metric-alerts/{id}/history returns each evaluation with the query behind it.

The registry has no status model. Repositories exist because something was pushed, so there is nothing to poll. /registry/preflight answers the question diagnose would.

The response envelope

Diagnostic endpoints return a consistent envelope:

JSON
{
  "success": true,
  "data": { },
  "error": null,
  "meta": { }
}

success reflects transport and authorisation only. A diagnose call that finds a fatal problem is still success: true — it succeeded at diagnosing. The verdict lives in data.

On failure, error carries a stable code and a retryable flag:

JSON
{
  "success": false,
  "data": null,
  "error": {
    "code": "cache.logs_unavailable",
    "message": "The log backend did not respond.",
    "retryable": true
  },
  "meta": {}
}

retryable is the field to branch on. true means the identical request may succeed later. false means it will not, and retrying only consumes your rate limit. An upstream that is unreachable returns HTTP 503 with retryable: true; a permission problem returns 403 with retryable: false.

Diagnostic responses are sent with Cache-Control: no-store, private. They can contain your log contents and must not be held by a shared cache.

Diagnose

GET /api/v1/{product}/{id}/diagnose correlates status, cluster events and log availability into a ranked list of findings:

JSON
{
  "verdict": "failed",
  "findings": [
    {
      "code": "cache.oom_killed",
      "severity": "fatal",
      "summary": "The container exceeded its memory limit and was killed.",
      "remediation": "Increase the memory profile, or reduce the working set.",
      "retryable": false
    }
  ],
  "status": { },
  "sections": {
    "events": { "available": true, "count": 12, "truncated": false },
    "logs":   { "available": true, "sampled_lines": 200 }
  }
}

Branch on finding.code, never on summary or remediation. Codes are a stable interface. The prose is written for humans and may be reworded at any time.

findings[0] is the most severe finding. Severities, in order:

SeverityMeaning
fatalBroken and will not recover on its own
action_requiredWorking, but needs a decision from you
transient_recoveredFailed and already recovered — not a current problem
informationalContext, not a problem

transient_recovered exists because Kubernetes emits warnings during perfectly normal reconciliation races. Reporting those as failures teaches operators to ignore diagnostics.

Sections degrade independently

If cluster events are unreachable, sections.events.available is false and diagnose still returns everything else. An empty section and an unavailable one are different answers, and the API never conflates them: available: true, count: 0 means "we looked and there was nothing".

Full code list: Failure codes.

Logs

GET /api/v1/{product}/{id}/logs requires {product}:diagnostics.

ParameterMeaning
sinceRelative (15m, 2h, 7d) or ISO-8601
levelFilter by severity where the workload emits one
limitLines to return
cursorOpaque; pass the previous response's cursor to continue

Retention is bounded — a since beyond the retention window returns what exists rather than failing.

Events

GET /api/v1/{product}/{id}/events returns platform-side Kubernetes events for the objects backing your resource, newest first, capped at 500 with a truncated flag. Events are the raw material /diagnose classifies; read them when you want to see what diagnose was looking at.

Rate limits

Diagnostic endpoints share a budget separate from the general API limit: 60 requests per minute per token and 180 per minute per project. An agent debugging many resources at once should diagnose serially rather than fanning out.

CLI exit codes

CodeMeaning
0Success
1Command failed, or a diagnose found a fatal problem
2Usage error
3Authentication or authorisation failure

--json prints the envelope verbatim on stdout, so a script can read the same structure the API returned.

Rules for agents

  1. Poll operation.terminal. Never the status string.
  2. Treat unknown enum values as "keep waiting". Domains grow additively.
  3. Branch on code, never on prose. finding.code and error.code are stable; message and summary are not.
  4. Respect retryable: false. Retrying wastes your rate limit and changes nothing.
  5. Read capabilities before calling a sub-resource. It exists so you do not have to probe.
  6. health: "unknown" during an operation is not a failure.
  7. degraded is not necessarily an outage. Check what is serving.
  8. stale: true is about our confidence, not your resource.
  9. Distinguish empty from unavailable. available: false means we could not look.
  10. Send X-Team-Id explicitly. Never rely on the account default.